fix: align backend APIs and upload flow
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
using System.Security.Claims;
|
||||
using IM.Admin.Data;
|
||||
using IM.InitCommon.Management;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IM.Admin.Api;
|
||||
|
||||
public sealed class ApiError(int status, string message) : Exception(message) { public int Status { get; } = status; }
|
||||
public static class ApiSupport
|
||||
{
|
||||
public static Guid Actor(this HttpContext c) => Guid.Parse(c.User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
public static string ActorName(this HttpContext c) => c.User.Identity?.Name ?? "";
|
||||
public static bool IsSuper(this HttpContext c) => c.User.IsInRole("super");
|
||||
public static void Reason(string? reason) { if (string.IsNullOrWhiteSpace(reason) || reason.Length > 500) throw new ApiError(400, "请填写 1–500 字的操作原因"); }
|
||||
public static void Audit(this AdminDb db, HttpContext c, string action, string target, string before, string after, string reason, Guid? reportId = null)
|
||||
=> db.Audit.Add(new AuditRecord { ActorId = c.Actor(), ActorName = c.ActorName(), Action = action, TargetId = target, TargetName = target, Before = before, After = after, Reason = reason, ReportId = reportId });
|
||||
public static async Task<PageResult<T>> Page<T>(IQueryable<T> query, int page, int size, CancellationToken ct)
|
||||
{ page = Math.Max(1, page); size = Math.Clamp(size, 1, 100); return new(await query.Skip((page - 1) * size).Take(size).ToListAsync(ct), await query.CountAsync(ct), page, size); }
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using IM.Admin.Data;
|
||||
using IM.Admin.Services;
|
||||
using IM.InitCommon.Management;
|
||||
using Microsoft.AspNetCore.Antiforgery;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IM.Admin.Api;
|
||||
public record LoginInput(string Account, string Password);
|
||||
public record PasswordInput(string CurrentPassword, string NewPassword);
|
||||
public record ResetRequest(string Account);
|
||||
public record ResetInput(string Token, string Password);
|
||||
public record AccountInput(string Account, string Name, string Email, string Password, string Role, bool Enabled, string Reason);
|
||||
public static class AuthEndpoints
|
||||
{
|
||||
public static object Public(AdminAccount a) => new { a.Id, a.Account, a.Name, a.Email, a.Role, status = a.Enabled ? "启用" : "停用", a.CreatedAt };
|
||||
public static string Hash(string value) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value)));
|
||||
public static void CheckPassword(string p) { if (p.Length is < 12 or > 128) throw new ApiError(400, "管理员密码长度应为 12–128 位"); }
|
||||
public static void MapAuth(this WebApplication app)
|
||||
{
|
||||
var api = app.MapGroup("/api/admin/auth");
|
||||
api.MapGet("/csrf", (HttpContext c, IAntiforgery af) => ManagementResult.Ok(new { token = af.GetAndStoreTokens(c).RequestToken }));
|
||||
api.MapPost("/login", async (LoginInput input, AdminDb db, IPasswordHasher<AdminAccount> hasher, SettingsService settings, HttpContext c) => {
|
||||
if (string.IsNullOrWhiteSpace(input.Account) || input.Account.Length > 100 || string.IsNullOrEmpty(input.Password) || input.Password.Length > 128) throw new ApiError(400, "账号或密码格式不正确");
|
||||
var name = input.Account.Trim().ToLowerInvariant();
|
||||
await using var tx = await db.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
|
||||
var a = await db.Accounts.FromSqlInterpolated($"SELECT * FROM admin_accounts WHERE Account = {name} FOR UPDATE").SingleOrDefaultAsync();
|
||||
var policy = await settings.Policy();
|
||||
if (a is null || !a.Enabled || a.LockedUntil > DateTime.UtcNow) throw new ApiError(401, "账号或密码错误,或账号暂不可用");
|
||||
if (hasher.VerifyHashedPassword(a, a.PasswordHash, input.Password) == PasswordVerificationResult.Failed) {
|
||||
a.FailedAttempts++; if (a.FailedAttempts >= policy.AdminLockThreshold) a.LockedUntil = DateTime.UtcNow.AddMinutes(policy.AdminLockMinutes);
|
||||
db.Audit.Add(new AuditRecord { ActorId = a.Id, ActorName = a.Name, Action = "后台登录", TargetId = a.Id.ToString(), Result = "失败", Reason = "密码验证未通过", After = a.LockedUntil.HasValue ? "临时锁定" : "登录失败" });
|
||||
await db.SaveChangesAsync(); await tx.CommitAsync(); throw new ApiError(401, "账号或密码错误,或账号暂不可用");
|
||||
}
|
||||
a.FailedAttempts = 0; a.LockedUntil = null;
|
||||
db.Audit.Add(new AuditRecord { ActorId = a.Id, ActorName = a.Name, Action = "后台登录", TargetId = a.Id.ToString(), Reason = "独立管理员登录", After = "登录成功" });
|
||||
await db.SaveChangesAsync(); await tx.CommitAsync();
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, a.Id.ToString()), new Claim(ClaimTypes.Name, a.Name), new Claim(ClaimTypes.Role, a.Role), new Claim("stamp", a.Stamp)], "Admin"));
|
||||
await c.SignInAsync("Admin", principal, new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(policy.AdminSessionMinutes) });
|
||||
return ManagementResult.Ok(Public(a));
|
||||
}).RequireRateLimiting("login");
|
||||
api.MapGet("/me", async (HttpContext c, AdminDb db) => ManagementResult.Ok(Public(await db.Accounts.SingleAsync(x => x.Id == c.Actor())))).RequireAuthorization();
|
||||
api.MapPost("/logout", async (HttpContext c) => { await c.SignOutAsync("Admin"); return ManagementResult.Ok(); }).RequireAuthorization();
|
||||
api.MapPost("/password", async (PasswordInput input, HttpContext c, AdminDb db, IPasswordHasher<AdminAccount> hasher) => {
|
||||
CheckPassword(input.NewPassword); var a = await db.Accounts.SingleAsync(x => x.Id == c.Actor());
|
||||
if (hasher.VerifyHashedPassword(a, a.PasswordHash, input.CurrentPassword) == PasswordVerificationResult.Failed) throw new ApiError(400, "当前密码不正确");
|
||||
a.PasswordHash = hasher.HashPassword(a, input.NewPassword); a.Stamp = Guid.NewGuid().ToString("N");
|
||||
db.Audit(c, "修改密码", a.Id.ToString(), "", "已更新", "管理员修改本人密码"); await db.SaveChangesAsync(); await c.SignOutAsync("Admin"); return ManagementResult.Ok();
|
||||
}).RequireAuthorization();
|
||||
api.MapPost("/forgot", async (ResetRequest input, AdminDb db, InfrastructureService mail, IConfiguration config) => {
|
||||
var a = await db.Accounts.SingleOrDefaultAsync(x => x.Account == input.Account.Trim().ToLowerInvariant() && x.Enabled);
|
||||
if (a is not null && !string.IsNullOrWhiteSpace(a.Email) && await mail.MailEnabled()) {
|
||||
var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
|
||||
db.Resets.Add(new PasswordReset { Id = Hash(token), AccountId = a.Id, ExpiresAt = DateTime.UtcNow.AddMinutes(20) }); await db.SaveChangesAsync();
|
||||
var origin = config["Management:AdminPublicUrl"] ?? throw new ApiError(503, "未配置后台访问地址");
|
||||
await mail.Send(a.Email, "IM 后台密码重置", $"请在 20 分钟内打开以下地址重置密码:{origin.TrimEnd('/')}/#/reset-password?token={token}\n如果不是你发起的请求,请忽略此邮件。");
|
||||
}
|
||||
return ManagementResult.Ok(new { message = "如果账号可用且已配置邮件,将收到重置说明。" });
|
||||
}).RequireRateLimiting("login");
|
||||
api.MapPost("/reset-password", async (ResetInput input, AdminDb db, IPasswordHasher<AdminAccount> hasher) => {
|
||||
CheckPassword(input.Password); await using var tx = await db.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
|
||||
var reset = await db.Resets.SingleOrDefaultAsync(x => x.Id == Hash(input.Token) && x.ExpiresAt > DateTime.UtcNow);
|
||||
if (reset is null) throw new ApiError(400, "链接无效或已过期");
|
||||
var a = await db.Accounts.SingleAsync(x => x.Id == reset.AccountId); if (!a.Enabled) throw new ApiError(400, "账号已停用");
|
||||
a.PasswordHash = hasher.HashPassword(a, input.Password); a.Stamp = Guid.NewGuid().ToString("N"); a.FailedAttempts = 0; a.LockedUntil = null;
|
||||
db.Resets.RemoveRange(await db.Resets.Where(x => x.AccountId == a.Id).ToListAsync()); db.Audit.Add(new AuditRecord { ActorId = a.Id, ActorName = a.Name, Action = "重置密码", TargetId = a.Id.ToString(), After = "已更新", Reason = "通过邮件重置密码" });
|
||||
await db.SaveChangesAsync(); await tx.CommitAsync(); return ManagementResult.Ok();
|
||||
}).RequireRateLimiting("login");
|
||||
var accounts = app.MapGroup("/api/admin/admins").RequireAuthorization("super");
|
||||
accounts.MapGet("", async (AdminDb db, string? q, string? status, string? role, int? page, int? size, CancellationToken ct) => {
|
||||
var query = db.Accounts.AsNoTracking().Where(x => q == null || x.Account.Contains(q) || x.Name.Contains(q));
|
||||
if (!string.IsNullOrEmpty(status)) query = query.Where(x => x.Enabled == (status == "启用"));
|
||||
if (!string.IsNullOrEmpty(role)) query = query.Where(x => x.Role == role);
|
||||
var result = await ApiSupport.Page(query.OrderBy(x => x.CreatedAt).Select(x => new { x.Id, x.Name, x.Account, x.Email, x.Role, status = x.Enabled ? "启用" : "停用", x.CreatedAt }), page ?? 1, size ?? 8, ct);
|
||||
return ManagementResult.Ok(result);
|
||||
});
|
||||
accounts.MapPost("", async (AccountInput input, AdminDb db, HttpContext c, IPasswordHasher<AdminAccount> hasher) => {
|
||||
Validate(input); CheckPassword(input.Password); var account = input.Account.Trim().ToLowerInvariant();
|
||||
if (await db.Accounts.AnyAsync(x => x.Account == account)) throw new ApiError(409, "管理员账号已存在");
|
||||
var a = new AdminAccount { Account = account, Name = input.Name.Trim(), Email = input.Email.Trim(), Role = input.Role, Enabled = input.Enabled }; a.PasswordHash = hasher.HashPassword(a, input.Password);
|
||||
db.Accounts.Add(a); db.Audit(c, "创建管理员", a.Id.ToString(), "", input.Role, input.Reason); await db.SaveChangesAsync(); return ManagementResult.Ok(Public(a));
|
||||
});
|
||||
accounts.MapPut("/{id:guid}", async (Guid id, AccountInput input, AdminDb db, HttpContext c) => {
|
||||
Validate(input); await using var tx = await db.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
|
||||
var a = await db.Accounts.SingleOrDefaultAsync(x => x.Id == id) ?? throw new ApiError(404, "管理员不存在");
|
||||
if (a.Id == c.Actor() && (input.Role != a.Role || !input.Enabled)) throw new ApiError(400, "不能停用或变更自己的角色");
|
||||
if (a.Role == "super" && a.Enabled && (input.Role != "super" || !input.Enabled) && await db.Accounts.CountAsync(x => x.Role == "super" && x.Enabled) <= 1) throw new ApiError(400, "必须保留一名启用的超级管理员");
|
||||
var before = $"{a.Role}/{a.Enabled}"; a.Name = input.Name.Trim(); a.Email = input.Email.Trim(); a.Role = input.Role; a.Enabled = input.Enabled; a.Stamp = Guid.NewGuid().ToString("N");
|
||||
db.Audit(c, "修改管理员", id.ToString(), before, $"{a.Role}/{a.Enabled}", input.Reason); await db.SaveChangesAsync(); await tx.CommitAsync(); return ManagementResult.Ok(Public(a));
|
||||
});
|
||||
}
|
||||
static void Validate(AccountInput i) {
|
||||
ApiSupport.Reason(i.Reason);
|
||||
if (i.Account.Length is < 3 or > 100 || string.IsNullOrWhiteSpace(i.Name) || i.Name.Length > 50 || !new[] { "super", "operator", "reviewer" }.Contains(i.Role)) throw new ApiError(400, "管理员资料不正确");
|
||||
if (!string.IsNullOrEmpty(i.Email) && !System.Net.Mail.MailAddress.TryCreate(i.Email, out _)) throw new ApiError(400, "邮箱格式不正确");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using IM.Admin.Data;
|
||||
using IM.Admin.Services;
|
||||
using IM.InitCommon.Management;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IM.Admin.Api;
|
||||
public record SubmitReport(Guid ReporterId, string Type, Guid TargetId, string Reason, string Description, Guid[] MessageIds);
|
||||
public record ReviewInput(Guid OperationId, string Action, string Reason);
|
||||
public static class BusinessEndpoints
|
||||
{
|
||||
public static void MapAdminBusiness(this WebApplication app)
|
||||
{
|
||||
var api = app.MapGroup("/api/admin").RequireAuthorization();
|
||||
api.MapGet("/dashboard", async (AdminDb db, InternalClient client) => {
|
||||
var users = client.Send<JsonElement>("user", "/internal/management/summary");
|
||||
var groups = client.Send<JsonElement>("group", "/internal/management/summary");
|
||||
await Task.WhenAll(users, groups);
|
||||
return ManagementResult.Ok(new { users = users.Result.GetProperty("total").GetInt32(), groups = groups.Result.GetProperty("total").GetInt32(), pending = await db.Reports.CountAsync(x => x.Status == "待处理"), disposals = await db.Operations.CountAsync(x => x.Status == "completed" && x.CompletedAt >= DateTime.UtcNow.Date) });
|
||||
});
|
||||
foreach (var resource in new[] { "users", "groups" }) {
|
||||
var service = resource == "users" ? "user" : "group";
|
||||
api.MapGet($"/{resource}", async (HttpContext c, InternalClient client) => ManagementResult.Ok(await client.Send<JsonElement>(service, "/internal/management/list" + c.Request.QueryString)));
|
||||
api.MapGet($"/{resource}/{{id:guid}}", async (Guid id, HttpContext c, InternalClient client, AdminDb db) => {
|
||||
var item = await client.Send<JsonElement>(service, $"/internal/management/detail/{id}");
|
||||
var reports = await db.Reports.AsNoTracking().Where(x => x.TargetId == id).OrderByDescending(x => x.CreatedAt).Select(x => new { x.Id, x.Reason, x.Status, x.Result, x.CreatedAt }).Take(100).ToListAsync();
|
||||
var logs = c.User.IsInRole("reviewer") ? [] : await db.Audit.AsNoTracking().Where(x => x.TargetId == id.ToString()).OrderByDescending(x => x.CreatedAt).Take(100).ToListAsync();
|
||||
return ManagementResult.Ok(new { item, reports, logs });
|
||||
});
|
||||
api.MapPost($"/{resource}/{{id:guid}}/actions", async (Guid id, ReviewInput input, AdminDb db, HttpContext c) => {
|
||||
if (input.Action is not "封禁" and not "解封") throw new ApiError(400, "操作不受支持");
|
||||
return await Enqueue(db, c, service, id, null, input);
|
||||
}).RequireAuthorization("operate");
|
||||
}
|
||||
api.MapGet("/reports", async (AdminDb db, string? q, string? status, string? type, int? page, int? size, CancellationToken ct) => {
|
||||
var query = db.Reports.AsNoTracking().Where(x => q == null || x.TargetName.Contains(q) || x.Reason.Contains(q) || x.Id.ToString() == q);
|
||||
if (!string.IsNullOrEmpty(status)) query = query.Where(x => x.Status == status);
|
||||
if (!string.IsNullOrEmpty(type)) query = query.Where(x => x.Type == type);
|
||||
return ManagementResult.Ok(await ApiSupport.Page(query.OrderByDescending(x => x.CreatedAt).Select(x => new { x.Id, x.TargetId, x.TargetName, x.Type, x.Reason, x.Status, x.AssigneeId, x.CreatedAt, x.Result, x.Version }), page ?? 1, size ?? 8, ct));
|
||||
});
|
||||
api.MapGet("/reports/{id:guid}", async (Guid id, AdminDb db, HttpContext c) => {
|
||||
var r = await db.Reports.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id) ?? throw new ApiError(404, "举报不存在");
|
||||
db.Audit(c, "查看举报证据", id.ToString(), "", "已访问", "审核证据读取", id); await db.SaveChangesAsync();
|
||||
var operations = await db.Operations.AsNoTracking().Where(x => x.ReportId == id).OrderByDescending(x => x.CreatedAt).ToListAsync();
|
||||
var history = await db.Reports.AsNoTracking().Where(x => x.TargetId == r.TargetId && x.Id != id && x.ClosedAt != null).OrderByDescending(x => x.ClosedAt).Select(x => new { x.Id, x.Result, x.Status, x.ClosedAt }).Take(30).ToListAsync();
|
||||
var name = r.AssigneeId is null ? null : await db.Accounts.Where(x => x.Id == r.AssigneeId).Select(x => x.Name).SingleOrDefaultAsync();
|
||||
return ManagementResult.Ok(new { r.Id, r.Type, r.TargetId, r.TargetName, r.ReporterId, r.Reason, r.Description, r.Status, r.AssigneeId, assigneeName = name, r.CreatedAt, r.ClosedAt, r.Result, r.Version, evidence = JsonSerializer.Deserialize<JsonElement>(r.Evidence), operations, history });
|
||||
});
|
||||
api.MapPost("/reports/{id:guid}/claim", async (Guid id, AdminDb db, HttpContext c) => {
|
||||
var r = await db.Reports.SingleOrDefaultAsync(x => x.Id == id) ?? throw new ApiError(404, "举报不存在");
|
||||
if (r.Status != "待处理") throw new ApiError(409, "举报已领取或已结案");
|
||||
r.Status = "处理中"; r.AssigneeId = c.Actor(); r.Version++;
|
||||
db.Audit(c, "领取举报", id.ToString(), "待处理", r.Status, "领取并核实举报", id); await db.SaveChangesAsync(); return ManagementResult.Ok();
|
||||
});
|
||||
api.MapPost("/reports/{id:guid}/review", async (Guid id, ReviewInput input, AdminDb db, HttpContext c) => {
|
||||
if (input.Action is not "警告" and not "驳回" and not "封禁") throw new ApiError(400, "操作不受支持");
|
||||
await using var tx = await db.Database.BeginTransactionAsync(IsolationLevel.Serializable);
|
||||
var r = await db.Reports.SingleOrDefaultAsync(x => x.Id == id) ?? throw new ApiError(404, "举报不存在");
|
||||
if (r.Status != "处理中" || r.AssigneeId != c.Actor()) throw new ApiError(409, "仅能处置本人领取且尚未结案的举报");
|
||||
if (await db.Operations.AnyAsync(x => x.ReportId == id && x.Status != "completed" && x.Id != input.OperationId)) throw new ApiError(409, "处置正在执行,请等待或重试原任务");
|
||||
var result = await Enqueue(db, c, r.Type, r.TargetId, id, input); await tx.CommitAsync(); return result;
|
||||
});
|
||||
api.MapGet("/operations/{id:guid}", async (Guid id, AdminDb db, HttpContext c) => {
|
||||
var op = await db.Operations.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id) ?? throw new ApiError(404, "任务不存在");
|
||||
if (c.User.IsInRole("reviewer") && op.ActorId != c.Actor()) throw new ApiError(403, "没有此任务权限"); return ManagementResult.Ok(op);
|
||||
});
|
||||
api.MapPost("/operations/{id:guid}/retry", async (Guid id, AdminDb db, HttpContext c) => {
|
||||
var op = await db.Operations.SingleOrDefaultAsync(x => x.Id == id) ?? throw new ApiError(404, "任务不存在");
|
||||
if (c.User.IsInRole("reviewer") && op.ActorId != c.Actor()) throw new ApiError(403, "没有此任务权限");
|
||||
if (op.Status != "failed") throw new ApiError(409, "仅失败任务可重试");
|
||||
op.Status = "pending"; op.Attempts = 0; op.Error = null; op.NextAttemptAt = DateTime.UtcNow; await db.SaveChangesAsync(); return ManagementResult.Ok(op);
|
||||
});
|
||||
api.MapGet("/logs", async (AdminDb db, string? q, Guid? actor, string? action, DateTime? from, DateTime? to, int? page, int? size, CancellationToken ct) => {
|
||||
var query = db.Audit.AsNoTracking().Where(x => q == null || x.TargetId.Contains(q) || x.TargetName.Contains(q) || x.Reason.Contains(q));
|
||||
if (actor.HasValue) query = query.Where(x => x.ActorId == actor);
|
||||
if (!string.IsNullOrEmpty(action)) query = query.Where(x => x.Action == action);
|
||||
if (from.HasValue) query = query.Where(x => x.CreatedAt >= from.Value);
|
||||
if (to.HasValue) { var end = to.Value.Date.AddDays(1); query = query.Where(x => x.CreatedAt < end); }
|
||||
return ManagementResult.Ok(await ApiSupport.Page(query.OrderByDescending(x => x.CreatedAt), page ?? 1, size ?? 8, ct));
|
||||
}).RequireAuthorization("operate");
|
||||
app.MapPost("/internal/management/reports", async (SubmitReport input, AdminDb db, SettingsService settings, InternalClient client) => {
|
||||
if (input.Type is not "user" and not "group" || input.Description.Length > 1000 || input.MessageIds.Length > 20) throw new ApiError(400, "举报参数不正确");
|
||||
var policy = await settings.Policy(); if (!policy.ReportCategories.Contains(input.Reason)) throw new ApiError(400, "请选择有效举报分类");
|
||||
var verified = await client.Send<SubjectEvidence>("message", "/internal/management/evidence", new EvidenceRequest(input.ReporterId, input.Type, input.TargetId, input.MessageIds));
|
||||
await using var tx = await db.Database.BeginTransactionAsync(IsolationLevel.Serializable);
|
||||
if (await db.Reports.CountAsync(x => x.ReporterId == input.ReporterId && x.CreatedAt >= DateTime.UtcNow.Date) >= policy.ReportsPerDay) throw new ApiError(429, "已达到今日举报上限");
|
||||
var cutoff = DateTime.UtcNow.AddMinutes(-policy.ReportCooldownMinutes);
|
||||
if (await db.Reports.AnyAsync(x => x.ReporterId == input.ReporterId && x.TargetId == input.TargetId && x.Type == input.Type && x.CreatedAt > cutoff)) throw new ApiError(429, "请勿重复举报同一对象");
|
||||
var r = new Report { ReporterId = input.ReporterId, TargetId = input.TargetId, TargetName = verified.TargetName, Type = input.Type, Reason = input.Reason, Description = input.Description, Evidence = JsonSerializer.Serialize(verified.Evidence, SettingsService.Json) };
|
||||
db.Reports.Add(r); await db.SaveChangesAsync(); await tx.CommitAsync(); return new { r.Id };
|
||||
});
|
||||
}
|
||||
static async Task<IResult> Enqueue(AdminDb db, HttpContext c, string type, Guid target, Guid? report, ReviewInput input)
|
||||
{
|
||||
ApiSupport.Reason(input.Reason); if (input.OperationId == Guid.Empty) throw new ApiError(400, "缺少操作 ID");
|
||||
var existing = await db.Operations.FindAsync(input.OperationId);
|
||||
if (existing is not null) {
|
||||
if (existing.ActorId != c.Actor() || existing.TargetId != target || existing.Action != input.Action || existing.ReportId != report || existing.Reason != input.Reason) throw new ApiError(409, "操作 ID 与已有请求冲突");
|
||||
return Results.Json(ManagementResult.Ok(existing), statusCode: 202);
|
||||
}
|
||||
var op = new Operation { Id = input.OperationId, ActorId = c.Actor(), ActorName = c.ActorName(), TargetId = target, Type = type, Action = input.Action, Reason = input.Reason, ReportId = report };
|
||||
db.Operations.Add(op); await db.SaveChangesAsync(); return Results.Json(ManagementResult.Ok(op), statusCode: 202);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Text.Json;
|
||||
using IM.Admin.Services;
|
||||
using IM.InitCommon.Management;
|
||||
|
||||
namespace IM.Admin.Api;
|
||||
public static class MonitoringEndpoints
|
||||
{
|
||||
public static void MapMonitoring(this WebApplication app)
|
||||
{
|
||||
var api = app.MapGroup("/api/admin").RequireAuthorization("operate");
|
||||
api.MapGet("/health", async (HealthSampler sampler, InternalClient client, CancellationToken ct) => {
|
||||
var health = await sampler.Read(ct); JsonElement? connections = null;
|
||||
try { connections = await client.Send<JsonElement>("connector", "/internal/management/connections", ct: ct); } catch { }
|
||||
return ManagementResult.Ok(new { services = health, connections, sampledAt = DateTime.UtcNow });
|
||||
});
|
||||
api.MapGet("/storage", async (HttpContext c, InternalClient client) => ManagementResult.Ok(await client.Send<JsonElement>("file", "/internal/management/storage/summary" + c.Request.QueryString, ct: c.RequestAborted)));
|
||||
app.MapGet("/api/admin/groups/{id:guid}/members", async (Guid id, HttpContext c, InternalClient client) => ManagementResult.Ok(await client.Send<JsonElement>("group", $"/internal/management/members/{id}" + c.Request.QueryString, ct: c.RequestAborted))).RequireAuthorization();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using IM.Admin.Data;
|
||||
using IM.Admin.Services;
|
||||
using IM.InitCommon.Management;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace IM.Admin.Api;
|
||||
public static class SettingsEndpoints
|
||||
{
|
||||
public static void MapSettings(this WebApplication app)
|
||||
{
|
||||
app.MapGet("/api/platform", async (SettingsService s) => { var p = await s.Policy(); return ManagementResult.Ok(new { p.PlatformName, p.Description, p.SupportEmail, p.RegistrationEnabled, p.PasswordMinLength, p.ReportCategories }); });
|
||||
app.MapGet("/internal/management/policy", async (SettingsService s) => await s.Policy());
|
||||
app.MapGet("/internal/management/infrastructure/{id}", async (string id, AdminDb db, SettingsService s) => {
|
||||
if (id != "storage") throw new ApiError(404, "不存在");
|
||||
var row = await db.Settings.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id);
|
||||
return new { version = row?.Version ?? 0, value = JsonNode.Parse(row?.Value ?? "{}"), secret = s.Unprotect(row?.Secret) };
|
||||
});
|
||||
var api = app.MapGroup("/api/admin/settings").RequireAuthorization("super");
|
||||
api.MapGet("", async (SettingsService s) => ManagementResult.Ok(await s.List()));
|
||||
api.MapPut("/{id}", async (string id, SettingInput input, SettingsService s, HttpContext c) => { await s.Save(id, input, c); return ManagementResult.Ok(await s.List()); });
|
||||
api.MapPost("/{id}/defaults", async (string id, SettingInput input, SettingsService s, HttpContext c) => { await s.Save(id, input with { Value = SettingsService.Defaults(id) }, c); return ManagementResult.Ok(await s.List()); });
|
||||
api.MapPost("/{id}/draft", async (string id, SettingInput input, InfrastructureService s, HttpContext c) => { await s.Draft(id, input, c); return ManagementResult.Ok(); });
|
||||
api.MapPost("/{id}/test", async (string id, InfraTest input, InfrastructureService s, HttpContext c) => { await s.Test(id, input, c); return ManagementResult.Ok(); });
|
||||
api.MapPost("/{id}/activate", async (string id, SettingInput input, InfrastructureService s, HttpContext c) => { await s.Activate(id, input, c); return ManagementResult.Ok(); });
|
||||
}
|
||||
}
|
||||
public record InfraTest(long Version, string? Recipient);
|
||||
Reference in New Issue
Block a user