Refactor app architecture and clean local artifacts
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
using Backend.Contracts;
|
||||
using Backend.Common;
|
||||
using Backend.Data;
|
||||
using Backend.Security;
|
||||
using Backend.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Endpoints;
|
||||
|
||||
public static partial class AdminModerationEndpoints
|
||||
{
|
||||
private static async Task<IResult> GetRiskFlags(
|
||||
int? limit,
|
||||
int? offset,
|
||||
string? status,
|
||||
string? severity,
|
||||
string? query,
|
||||
bool? reviewedOnly,
|
||||
AwardsDbContext db)
|
||||
{
|
||||
var normalizedLimit = Math.Clamp(limit ?? 25, 1, 100);
|
||||
var normalizedOffset = Math.Max(offset ?? 0, 0);
|
||||
var normalizedStatus = NormalizeRiskFlagStatus(status, allowAll: true);
|
||||
if (normalizedStatus is null)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Risk flag status must be open, resolved, dismissed or all." });
|
||||
}
|
||||
|
||||
var riskQuery = db.RiskFlags.AsNoTracking();
|
||||
|
||||
if (reviewedOnly == true)
|
||||
{
|
||||
riskQuery = riskQuery.Where(item => item.Status != "open");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(severity) && !string.Equals(severity, "all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var normalizedSeverity = severity.Trim().ToLowerInvariant();
|
||||
if (normalizedSeverity is not ("low" or "medium" or "high"))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Risk flag severity must be low, medium, high or all." });
|
||||
}
|
||||
|
||||
riskQuery = riskQuery.Where(item => item.Severity == normalizedSeverity);
|
||||
}
|
||||
|
||||
if (normalizedStatus != "all")
|
||||
{
|
||||
riskQuery = riskQuery.Where(item => item.Status == normalizedStatus);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
var pattern = $"%{query.Trim()}%";
|
||||
riskQuery = riskQuery.Where(item =>
|
||||
EF.Functions.ILike(item.Source, pattern) ||
|
||||
EF.Functions.ILike(item.Type, pattern) ||
|
||||
EF.Functions.ILike(item.Severity, pattern) ||
|
||||
EF.Functions.ILike(item.Status, pattern) ||
|
||||
EF.Functions.ILike(item.Summary, pattern) ||
|
||||
(item.TwitchUserId != null && EF.Functions.ILike(item.TwitchUserId, pattern)) ||
|
||||
EF.Functions.ILike(item.CreatedFromIp, pattern) ||
|
||||
(item.ReviewNote != null && EF.Functions.ILike(item.ReviewNote, pattern)) ||
|
||||
EF.Functions.ILike(item.MetadataJson, pattern));
|
||||
}
|
||||
|
||||
var totalCount = await riskQuery.CountAsync();
|
||||
var severityCounts = await riskQuery
|
||||
.GroupBy(item => item.Severity)
|
||||
.Select(group => new AdminRiskCountDto(group.Key, group.Count()))
|
||||
.ToArrayAsync();
|
||||
var statusCounts = await riskQuery
|
||||
.GroupBy(item => item.Status)
|
||||
.Select(group => new AdminRiskCountDto(group.Key, group.Count()))
|
||||
.ToArrayAsync();
|
||||
|
||||
var flags = await riskQuery
|
||||
.OrderByDescending(item => item.CreatedAt)
|
||||
.Skip(normalizedOffset)
|
||||
.Take(normalizedLimit)
|
||||
.ToArrayAsync();
|
||||
|
||||
var items = flags.Select(AdminRiskFlagMappings.ToDto).ToArray();
|
||||
|
||||
return Results.Ok(new AdminRiskFlagsResponse(
|
||||
items,
|
||||
totalCount,
|
||||
items.Length,
|
||||
normalizedOffset,
|
||||
normalizedLimit,
|
||||
normalizedOffset + items.Length < totalCount,
|
||||
severityCounts,
|
||||
statusCounts));
|
||||
}
|
||||
|
||||
private static async Task<IResult> ResolveRiskFlag(
|
||||
HttpContext context,
|
||||
int riskFlagId,
|
||||
ResolveRiskFlagRequest request,
|
||||
AwardsDbContext db,
|
||||
IAdminAuditService adminAuditService)
|
||||
{
|
||||
var session = AdminEndpointConventions.CurrentSession(context);
|
||||
var riskFlag = await db.RiskFlags.FirstOrDefaultAsync(item => item.Id == riskFlagId);
|
||||
if (riskFlag is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var normalizedStatus = NormalizeRiskFlagStatus(request.Status, allowAll: false);
|
||||
if (normalizedStatus is null)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Risk flag status must be open, resolved or dismissed." });
|
||||
}
|
||||
|
||||
var previousStatus = riskFlag.Status;
|
||||
var previousReviewNote = riskFlag.ReviewNote;
|
||||
var normalizedReviewNote = NormalizeReviewNote(request.ReviewNote);
|
||||
|
||||
riskFlag.Status = normalizedStatus;
|
||||
if (normalizedStatus == "open")
|
||||
{
|
||||
riskFlag.ReviewedAt = null;
|
||||
riskFlag.ReviewedByTwitchId = null;
|
||||
riskFlag.ReviewNote = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
riskFlag.ReviewedAt = DateTimeOffset.UtcNow;
|
||||
riskFlag.ReviewedByTwitchId = session.TwitchUserId;
|
||||
riskFlag.ReviewNote = normalizedReviewNote;
|
||||
}
|
||||
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
"risk.resolve",
|
||||
"risk-flag",
|
||||
riskFlag.Id.ToString(),
|
||||
$"Risk Flag {riskFlag.Id} wurde als {riskFlag.Status} markiert.",
|
||||
new
|
||||
{
|
||||
riskFlag.Type,
|
||||
riskFlag.Source,
|
||||
changes = BuildRiskResolutionChanges(previousStatus, riskFlag.Status, previousReviewNote, riskFlag.ReviewNote),
|
||||
},
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(new { saved = true, riskFlagId = riskFlag.Id, status = riskFlag.Status });
|
||||
}
|
||||
|
||||
private static async Task<IResult> BulkResolveRiskFlags(
|
||||
HttpContext context,
|
||||
BulkResolveRiskFlagsRequest request,
|
||||
AwardsDbContext db,
|
||||
IAdminAuditService adminAuditService)
|
||||
{
|
||||
var session = AdminEndpointConventions.CurrentSession(context);
|
||||
var normalizedIds = request.RiskFlagIds
|
||||
.Distinct()
|
||||
.Take(100)
|
||||
.ToArray();
|
||||
if (normalizedIds.Length == 0)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Bitte waehle mindestens einen Risikohinweis aus." });
|
||||
}
|
||||
|
||||
var normalizedStatus = NormalizeRiskFlagStatus(request.Status, allowAll: false);
|
||||
if (normalizedStatus is null || normalizedStatus == "open")
|
||||
{
|
||||
return Results.BadRequest(new { message = "Bulk-Verarbeitung unterstuetzt erledigt oder verworfen." });
|
||||
}
|
||||
|
||||
var normalizedReviewNote = NormalizeReviewNote(request.ReviewNote);
|
||||
if (string.IsNullOrWhiteSpace(normalizedReviewNote))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Bulk-Verarbeitung braucht eine Review-Notiz." });
|
||||
}
|
||||
|
||||
var riskFlags = await db.RiskFlags
|
||||
.Where(item => normalizedIds.Contains(item.Id))
|
||||
.ToArrayAsync(context.RequestAborted);
|
||||
|
||||
if (riskFlags.Length != normalizedIds.Length)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Mindestens ein Risikohinweis wurde nicht gefunden." });
|
||||
}
|
||||
|
||||
if (riskFlags.Any(item => item.Severity != "low"))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Bulk-Verarbeitung ist nur fuer Low-Severity-Hinweise erlaubt." });
|
||||
}
|
||||
|
||||
if (riskFlags.Any(item => item.Status != "open"))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Bulk-Verarbeitung ist nur fuer offene Hinweise erlaubt." });
|
||||
}
|
||||
|
||||
var reviewedAt = DateTimeOffset.UtcNow;
|
||||
foreach (var riskFlag in riskFlags)
|
||||
{
|
||||
riskFlag.Status = normalizedStatus;
|
||||
riskFlag.ReviewNote = normalizedReviewNote;
|
||||
riskFlag.ReviewedAt = reviewedAt;
|
||||
riskFlag.ReviewedByTwitchId = session.TwitchUserId;
|
||||
}
|
||||
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
"risk.bulk-resolve",
|
||||
"risk-flag",
|
||||
string.Join(",", normalizedIds),
|
||||
$"{normalizedIds.Length} Low-Risk Flags wurden als {normalizedStatus} markiert.",
|
||||
new
|
||||
{
|
||||
status = normalizedStatus,
|
||||
count = normalizedIds.Length,
|
||||
riskFlagIds = normalizedIds,
|
||||
changes = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
field = "status",
|
||||
label = "Status",
|
||||
from = "open",
|
||||
to = normalizedStatus,
|
||||
sensitive = false,
|
||||
},
|
||||
new
|
||||
{
|
||||
field = "reviewNote",
|
||||
label = "Review-Notiz",
|
||||
from = "keine Notiz",
|
||||
to = "Notiz vorhanden",
|
||||
sensitive = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(new { saved = true, count = normalizedIds.Length, status = normalizedStatus });
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetRiskRules(AwardsDbContext db)
|
||||
{
|
||||
var settings = await db.SiteSettings
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(item => item.Id == 1);
|
||||
if (settings is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(new AdminRiskRulesResponse(RiskRuleSettings.Read(settings).Select(ToRiskRuleDto).ToArray()));
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateRiskRules(
|
||||
HttpContext context,
|
||||
UpdateRiskRulesRequest request,
|
||||
AwardsDbContext db,
|
||||
IAdminAuditService adminAuditService)
|
||||
{
|
||||
var session = AdminEndpointConventions.CurrentSession(context);
|
||||
if (!AdminRoles.CanManageAdminWorkspace(session.Role))
|
||||
{
|
||||
return Results.Json(new { message = "Risk-Regeln koennen nur Admins oder Owner aendern." }, statusCode: StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
|
||||
if (settings is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var before = RiskRuleSettings.Read(settings);
|
||||
var mergedRules = RiskRuleSettings.Defaults
|
||||
.Select(defaultRule =>
|
||||
{
|
||||
var requestRule = request.Rules.FirstOrDefault(item => item.Key == defaultRule.Key);
|
||||
return requestRule is null
|
||||
? defaultRule
|
||||
: new RiskRuleSetting(
|
||||
defaultRule.Key,
|
||||
defaultRule.Label,
|
||||
requestRule.Enabled,
|
||||
requestRule.Threshold,
|
||||
requestRule.WindowMinutes,
|
||||
requestRule.Severity,
|
||||
defaultRule.Description);
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
settings.RiskRulesJson = RiskRuleSettings.Serialize(mergedRules);
|
||||
var after = RiskRuleSettings.Read(settings);
|
||||
var changes = after
|
||||
.Select(rule =>
|
||||
{
|
||||
var previous = before.First(item => item.Key == rule.Key);
|
||||
return new
|
||||
{
|
||||
field = rule.Key,
|
||||
label = rule.Label,
|
||||
from = $"{previous.Enabled}/{previous.Threshold}/{previous.WindowMinutes}/{previous.Severity}",
|
||||
to = $"{rule.Enabled}/{rule.Threshold}/{rule.WindowMinutes}/{rule.Severity}",
|
||||
sensitive = false,
|
||||
};
|
||||
})
|
||||
.Where(change => change.from != change.to)
|
||||
.ToArray();
|
||||
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
"risk-rules.update",
|
||||
"site-settings",
|
||||
settings.Id.ToString(),
|
||||
"Risk-Regeln wurden aktualisiert.",
|
||||
new { changes },
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(new AdminRiskRulesResponse(after.Select(ToRiskRuleDto).ToArray()));
|
||||
}
|
||||
|
||||
private static AdminRiskRuleDto ToRiskRuleDto(RiskRuleSetting rule) =>
|
||||
new(rule.Key, rule.Label, rule.Enabled, rule.Threshold, rule.WindowMinutes, rule.Severity, rule.Description);
|
||||
|
||||
private static string? NormalizeReviewNote(string? reviewNote)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reviewNote))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmedReviewNote = reviewNote.Trim();
|
||||
return trimmedReviewNote.Length <= 500 ? trimmedReviewNote : trimmedReviewNote[..500];
|
||||
}
|
||||
|
||||
private static object[] BuildRiskResolutionChanges(
|
||||
string previousStatus,
|
||||
string currentStatus,
|
||||
string? previousReviewNote,
|
||||
string? currentReviewNote)
|
||||
{
|
||||
var changes = new List<object>
|
||||
{
|
||||
new
|
||||
{
|
||||
field = "status",
|
||||
label = "Status",
|
||||
from = previousStatus,
|
||||
to = currentStatus,
|
||||
sensitive = false,
|
||||
},
|
||||
};
|
||||
|
||||
if (!string.Equals(previousReviewNote, currentReviewNote, StringComparison.Ordinal))
|
||||
{
|
||||
changes.Add(new
|
||||
{
|
||||
field = "reviewNote",
|
||||
label = "Review-Notiz",
|
||||
from = string.IsNullOrWhiteSpace(previousReviewNote) ? "keine Notiz" : "Notiz vorhanden",
|
||||
to = string.IsNullOrWhiteSpace(currentReviewNote) ? "keine Notiz" : "Notiz vorhanden",
|
||||
sensitive = true,
|
||||
});
|
||||
}
|
||||
|
||||
return changes.ToArray();
|
||||
}
|
||||
|
||||
private static string? NormalizeRiskFlagStatus(string? status, bool allowAll)
|
||||
{
|
||||
var normalizedStatus = string.IsNullOrWhiteSpace(status) ? "open" : status.Trim().ToLowerInvariant();
|
||||
return normalizedStatus switch
|
||||
{
|
||||
"open" or "resolved" or "dismissed" => normalizedStatus,
|
||||
"all" when allowAll => normalizedStatus,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user