Files
vtuber-awards/Backend/Services/NominationLinkBlacklistSettings.cs
T
AzuTear 18b61bed52 Improve admin candidate modal UX and add clip menu visibility toggle
- Widen AdminCandidateEditorModal to size lg for better readability
- Rename "Clip-Compilation" section to "Clip / Compilation", update copy to reflect single clips too, drop upload hint and Clip-Plattform field, rename label to "Link"
- Fix NativeSelect dropdown clipping inside overflow-y-auto modals by teleporting the menu to body with fixed positioning, flip-up logic, and dynamic maxHeight capped to viewport
- Add ClipAdminMenuVisible setting (backend domain, contracts, endpoint, migration) with matching frontend types, defaults, form wiring, and toggle in the Clip-Workflow modal — hides the Clips nav item from the admin sidebar when disabled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 18:39:35 +02:00

122 lines
3.7 KiB
C#

using System.Text.Json;
using Backend.Domain;
namespace Backend.Services;
public sealed record NominationLinkBlacklistEntry(string Url);
public static class NominationLinkBlacklistSettings
{
public static readonly NominationLinkBlacklistEntry[] Defaults =
[
new("https://www.twitch.tv/"),
new("https://kick.com/"),
new("https://www.youtube.com/"),
];
public static NominationLinkBlacklistEntry[] Read(SiteSettings? settings)
{
var entries = Parse(settings?.NominationLinkBlacklistJson);
return entries.Length > 0 ? entries : Defaults;
}
public static string Serialize(IEnumerable<NominationLinkBlacklistEntry> entries)
{
var normalizedEntries = entries
.Select(entry => NormalizeEntry(entry.Url))
.Where(entry => entry is not null)
.Select(entry => new NominationLinkBlacklistEntry(entry!))
.DistinctBy(entry => BuildComparisonKey(entry.Url), StringComparer.OrdinalIgnoreCase)
.OrderBy(entry => entry.Url, StringComparer.OrdinalIgnoreCase)
.ToArray();
return JsonSerializer.Serialize(normalizedEntries);
}
public static bool TryNormalizeUrl(string? rawUrl, out string normalizedUrl)
{
normalizedUrl = NormalizeEntry(rawUrl) ?? string.Empty;
return !string.IsNullOrWhiteSpace(normalizedUrl);
}
public static bool IsBlocked(string rawUrl, IEnumerable<NominationLinkBlacklistEntry> entries)
{
var submittedKey = BuildComparisonKey(rawUrl);
return !string.IsNullOrWhiteSpace(submittedKey)
&& entries.Any(entry => string.Equals(BuildComparisonKey(entry.Url), submittedKey, StringComparison.OrdinalIgnoreCase));
}
private static NominationLinkBlacklistEntry[] Parse(string? json)
{
if (string.IsNullOrWhiteSpace(json))
{
return [];
}
try
{
var entries = JsonSerializer.Deserialize<NominationLinkBlacklistEntry[]>(json);
return entries?.Where(entry => !string.IsNullOrWhiteSpace(entry.Url)).ToArray() ?? [];
}
catch (JsonException)
{
return [];
}
}
private static string? NormalizeEntry(string? rawUrl)
{
if (string.IsNullOrWhiteSpace(rawUrl))
{
return null;
}
var candidate = rawUrl.Trim();
if (!candidate.Contains("://", StringComparison.Ordinal))
{
candidate = $"https://{candidate}";
}
if (!Uri.TryCreate(candidate, UriKind.Absolute, out var uri))
{
return null;
}
if (uri.Scheme is not ("http" or "https") || string.IsNullOrWhiteSpace(uri.Host))
{
return null;
}
var builder = new UriBuilder(uri)
{
Scheme = Uri.UriSchemeHttps,
Host = uri.Host.ToLowerInvariant(),
Port = -1,
Query = string.Empty,
Fragment = string.Empty,
};
var normalized = builder.Uri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped);
return normalized.EndsWith('/') ? normalized : $"{normalized}/";
}
private static string BuildComparisonKey(string? rawUrl)
{
if (NormalizeEntry(rawUrl) is not { } normalized)
{
return string.Empty;
}
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri))
{
return string.Empty;
}
var host = uri.Host.StartsWith("www.", StringComparison.OrdinalIgnoreCase)
? uri.Host[4..]
: uri.Host;
var path = uri.AbsolutePath.TrimEnd('/');
return $"{host.ToLowerInvariant()}{path.ToLowerInvariant()}";
}
}