feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
102
apps/api-server/Services/AdminOperationService.cs
Normal file
102
apps/api-server/Services/AdminOperationService.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using D3ROVoice.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace D3ROVoice.Api.Services;
|
||||
|
||||
public sealed class AdminOperationException : Exception
|
||||
{
|
||||
public AdminOperationException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
public interface IAdminOperationService
|
||||
{
|
||||
Task<JsonElement> ExecuteAsync(
|
||||
string actorEmail,
|
||||
string operation,
|
||||
string idempotencyKey,
|
||||
object request,
|
||||
string targetType,
|
||||
Func<object, string> targetId,
|
||||
string memo,
|
||||
Func<Task<object?>> readBefore,
|
||||
Func<Task<object>> mutate);
|
||||
}
|
||||
|
||||
public sealed class AdminOperationService : IAdminOperationService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public AdminOperationService(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<JsonElement> ExecuteAsync(
|
||||
string actorEmail,
|
||||
string operation,
|
||||
string idempotencyKey,
|
||||
object request,
|
||||
string targetType,
|
||||
Func<object, string> targetId,
|
||||
string memo,
|
||||
Func<Task<object?>> readBefore,
|
||||
Func<Task<object>> mutate)
|
||||
{
|
||||
actorEmail = actorEmail.Trim().ToLowerInvariant();
|
||||
memo = memo.Trim();
|
||||
if (actorEmail.Length is < 3 or > 150) throw new AdminOperationException("invalid_actor");
|
||||
if (!Guid.TryParseExact(idempotencyKey, "D", out _)) throw new AdminOperationException("invalid_idempotency_key");
|
||||
if (memo.Length is < 3 or > 1000) throw new AdminOperationException("invalid_audit_memo");
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(request);
|
||||
var requestHash = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes($"{operation}:{requestJson}")));
|
||||
|
||||
await using var transaction = await _db.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
|
||||
var existing = await _db.AdminOperationRequests.AsNoTracking().SingleOrDefaultAsync(entry =>
|
||||
entry.ActorEmail == actorEmail && entry.IdempotencyKey == idempotencyKey);
|
||||
if (existing != null)
|
||||
{
|
||||
if (existing.Operation != operation || existing.RequestHash != requestHash)
|
||||
throw new AdminOperationException("idempotency_key_reused_with_different_request");
|
||||
using var replay = JsonDocument.Parse(existing.ResponseJson);
|
||||
return replay.RootElement.Clone();
|
||||
}
|
||||
|
||||
var before = await readBefore();
|
||||
var result = await mutate();
|
||||
var responseJson = JsonSerializer.Serialize(result);
|
||||
var resolvedTargetId = targetId(result);
|
||||
if (string.IsNullOrWhiteSpace(resolvedTargetId) || resolvedTargetId.Length > 200)
|
||||
throw new AdminOperationException("invalid_audit_target");
|
||||
|
||||
_db.AdminOperationRequests.Add(new AdminOperationRequest
|
||||
{
|
||||
ActorEmail = actorEmail,
|
||||
IdempotencyKey = idempotencyKey,
|
||||
Operation = operation,
|
||||
RequestHash = requestHash,
|
||||
ResponseJson = responseJson,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
_db.AdminAuditEntries.Add(new AdminAuditEntry
|
||||
{
|
||||
ActorEmail = actorEmail,
|
||||
Action = operation,
|
||||
TargetType = targetType,
|
||||
TargetId = resolvedTargetId,
|
||||
BeforeJson = before == null ? null : JsonSerializer.Serialize(before),
|
||||
AfterJson = responseJson,
|
||||
Memo = memo,
|
||||
IdempotencyKey = idempotencyKey,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
using var response = JsonDocument.Parse(responseJson);
|
||||
return response.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue