18b61bed52
- 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>
122 lines
3.7 KiB
C#
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()}";
|
|
}
|
|
}
|