新增 POST /api/admin/notices、PATCH /api/admin/notices/{id}
保留超级管理员权限边界 支持草稿、发布、置顶、自动摘要 HTML 净化,拦截脚本和危险链接 公告更新与审计日志同事务提交 新增开关 ADMIN_NATIVE_NOTICE_WRITES_ENABLED=false 管理端公告列表和系统自动公告暂时仍由 Node 处理
This commit is contained in:
1 parent
3665aa3aa9
commit
9e21a277f0
14 files changed
+350
-21
No files matched your search
@@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Eis.Application.Administration;
|
||||
|
||||
public interface IAdminNoticeService
|
||||
{
|
||||
Task<AdminEndpointResult> CreateAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
|
||||
Task<AdminEndpointResult> UpdateAsync(string sessionToken, string noticeId, JsonObject body, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -4,7 +4,8 @@ public sealed record AdminMigrationOptions(
|
||||
bool NativeReadsEnabled,
|
||||
bool NativeOrganizationWritesEnabled = false,
|
||||
bool NativeAccountBatchesEnabled = false,
|
||||
bool NativeConfigurationEnabled = false)
|
||||
bool NativeConfigurationEnabled = false,
|
||||
bool NativeNoticeWritesEnabled = false)
|
||||
{
|
||||
public static AdminMigrationOptions FromEnvironment(
|
||||
bool configuredNativeReadsEnabled,
|
||||
@@ -12,7 +13,8 @@ public sealed record AdminMigrationOptions(
|
||||
bool sharesLegacySessions,
|
||||
bool configuredNativeOrganizationWritesEnabled = false,
|
||||
bool configuredNativeAccountBatchesEnabled = false,
|
||||
bool configuredNativeConfigurationEnabled = false)
|
||||
bool configuredNativeConfigurationEnabled = false,
|
||||
bool configuredNativeNoticeWritesEnabled = false)
|
||||
{
|
||||
var readsEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED"),
|
||||
@@ -26,12 +28,15 @@ public sealed record AdminMigrationOptions(
|
||||
var configurationEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_CONFIGURATION_ENABLED"),
|
||||
configuredNativeConfigurationEnabled);
|
||||
if ((organizationWritesEnabled || accountBatchesEnabled || configurationEnabled) && !readsEnabled)
|
||||
var noticeWritesEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_NOTICE_WRITES_ENABLED"),
|
||||
configuredNativeNoticeWritesEnabled);
|
||||
if ((organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeWritesEnabled) && !readsEnabled)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"启用原生组织维护接口前必须同时设置 ADMIN_NATIVE_READS_ENABLED=true");
|
||||
}
|
||||
var anyNativeAdminEndpointEnabled = readsEnabled || organizationWritesEnabled || accountBatchesEnabled || configurationEnabled;
|
||||
var anyNativeAdminEndpointEnabled = readsEnabled || organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeWritesEnabled;
|
||||
if (anyNativeAdminEndpointEnabled && !authenticationNativeEnabled)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
@@ -47,7 +52,7 @@ public sealed record AdminMigrationOptions(
|
||||
"管理端仍有接口需要转发给 Node;启用原生管理端接口必须配置共享 Redis 会话");
|
||||
}
|
||||
|
||||
return new AdminMigrationOptions(readsEnabled, organizationWritesEnabled, accountBatchesEnabled, configurationEnabled);
|
||||
return new AdminMigrationOptions(readsEnabled, organizationWritesEnabled, accountBatchesEnabled, configurationEnabled, noticeWritesEnabled);
|
||||
}
|
||||
|
||||
private static bool ParseBoolean(string? value, bool fallback) => value?.Trim().ToLowerInvariant() switch
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using Eis.Infrastructure.Data;
|
||||
|
||||
namespace Eis.Infrastructure.Administration;
|
||||
|
||||
internal sealed record AdminNotice(string Id, string Title, string Summary, string Content, string Category, bool Pinned, string Status, string? PublishAt, string? CreatedAt, string Author);
|
||||
|
||||
internal sealed class AdminNoticeRepository(IRelationalConnectionFactory connectionFactory)
|
||||
{
|
||||
public async Task<AdminNotice?> FindAsync(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT id, title, summary, content, category, pinned, status, publish_at, created_at, author FROM notices WHERE id = @id LIMIT 1";
|
||||
Add(command, "@id", id);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
return await reader.ReadAsync(cancellationToken) ? Read(reader) : null;
|
||||
}
|
||||
|
||||
public Task CreateAsync(AdminNotice notice, AdminAuditEntry audit, CancellationToken cancellationToken) =>
|
||||
ExecuteAsync("""
|
||||
INSERT INTO notices (id, title, summary, content, category, pinned, status, publish_at, created_at, author)
|
||||
VALUES (@id, @title, @summary, @content, @category, @pinned, @status, @publishAt, @createdAt, @author)
|
||||
""", notice, audit, cancellationToken);
|
||||
|
||||
public Task UpdateAsync(AdminNotice notice, AdminAuditEntry audit, CancellationToken cancellationToken) =>
|
||||
ExecuteAsync("""
|
||||
UPDATE notices SET title = @title, summary = @summary, content = @content, category = @category,
|
||||
pinned = @pinned, status = @status, publish_at = @publishAt WHERE id = @id
|
||||
""", notice, audit, cancellationToken);
|
||||
|
||||
private async Task ExecuteAsync(string sql, AdminNotice notice, AdminAuditEntry audit, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = sql;
|
||||
Add(command, "@id", notice.Id); Add(command, "@title", notice.Title); Add(command, "@summary", notice.Summary);
|
||||
Add(command, "@content", notice.Content); Add(command, "@category", notice.Category); Add(command, "@pinned", notice.Pinned ? 1 : 0);
|
||||
Add(command, "@status", notice.Status); Add(command, "@publishAt", notice.PublishAt); Add(command, "@createdAt", notice.CreatedAt); Add(command, "@author", notice.Author);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
await using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = "INSERT INTO audit_logs (id, actor_id, action, detail, created_at) VALUES (@id, @actorId, @action, @detail, @createdAt)";
|
||||
Add(command, "@id", audit.Id); Add(command, "@actorId", audit.ActorId); Add(command, "@action", audit.Action); Add(command, "@detail", audit.Detail); Add(command, "@createdAt", audit.CreatedAt);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static AdminNotice Read(DbDataReader reader) => new(
|
||||
Text(reader, "id"), Text(reader, "title"), Text(reader, "summary"), Text(reader, "content"), Text(reader, "category"),
|
||||
Convert.ToInt64(reader.GetValue(reader.GetOrdinal("pinned")), CultureInfo.InvariantCulture) != 0, Text(reader, "status"),
|
||||
Optional(reader, "publish_at"), Optional(reader, "created_at"), Text(reader, "author"));
|
||||
private static string Text(DbDataReader reader, string name) => Convert.ToString(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture) ?? "";
|
||||
private static string? Optional(DbDataReader reader, string name) { var index = reader.GetOrdinal(name); return reader.IsDBNull(index) ? null : Convert.ToString(reader.GetValue(index), CultureInfo.InvariantCulture); }
|
||||
private static void Add(DbCommand command, string name, object? value) { var parameter = command.CreateParameter(); parameter.ParameterName = name; parameter.Value = value ?? DBNull.Value; command.Parameters.Add(parameter); }
|
||||
}
|
||||
Loaded 3 of 14 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user