Add winner archive, host image upload and live demo data
Deliver the demo-ready feature set and seed data so the live site can be presented end to end: - Winner archive: ArchivedWinner domain, admin CRUD endpoints/view/manager and public archive surface, backed by AddArchivedWinners migration. - Host presentation: host image upload and artist name on SiteSettings with public image endpoint and supporting migrations. - Clip submissions: idempotent table-ensure migration plus current-season demo clips for review workflows. - Demo seed data: sponsors, share links and 2025 archived winners, with a guarded RemoveDemoSeasons cleanup; all seeds guard against real data. - EnsureRuntimeSchemaParity migration to align runtime schema defensively. - Admin/home UI refinements; remove unused team role permissions modal and dead share-quick-links code. All seed and schema migrations are idempotent (IF NOT EXISTS / ON CONFLICT) and skip when real season data is present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,15 @@ public static class AdminSiteSettingsEndpoints
|
||||
private const string FallbackMaintenanceTitle = "Sternenpause";
|
||||
private const string FallbackMaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.";
|
||||
private const string FallbackClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.";
|
||||
private const string DefaultHostImageUrl = "/assets/amaterasu2sei_2.png";
|
||||
private const int MaxHostImageBytes = 8 * 1024 * 1024;
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string> AllowedHostImageContentTypes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["image/png"] = ".png",
|
||||
["image/jpeg"] = ".jpg",
|
||||
["image/webp"] = ".webp",
|
||||
};
|
||||
|
||||
public static RouteGroupBuilder MapAdminSiteSettingsEndpoints(this RouteGroupBuilder group)
|
||||
{
|
||||
@@ -25,6 +34,10 @@ public static class AdminSiteSettingsEndpoints
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Content))
|
||||
.WithName("UpdateAdminSiteSettings")
|
||||
.WithOpenApi();
|
||||
group.MapPost("/site-settings/host-image", UploadHostImage)
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Content))
|
||||
.WithName("UploadAdminHostImage")
|
||||
.WithOpenApi();
|
||||
group.MapGet("/operational-settings", GetOperationalSettings)
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequirePermission(context, next, AdminPermissionCatalog.Settings))
|
||||
.WithName("GetAdminOperationalSettings")
|
||||
@@ -68,43 +81,76 @@ public static class AdminSiteSettingsEndpoints
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(new AdminSiteSettingsResponse(
|
||||
settings.HostDisplayName,
|
||||
settings.HostTagline,
|
||||
settings.NewsletterUrl,
|
||||
settings.ShareXUrl,
|
||||
settings.ShareDiscordUrl,
|
||||
settings.PrivacyEmail,
|
||||
settings.PrivacyPolicyContent,
|
||||
settings.PrivacyPolicyUpdatedBy,
|
||||
settings.PrivacyPolicyUpdatedAt,
|
||||
settings.ImprintUrl,
|
||||
settings.ImprintContent,
|
||||
settings.ContactUrl,
|
||||
settings.ContactContent,
|
||||
settings.SponsorsUrl,
|
||||
settings.SponsorsContent,
|
||||
settings.ShowactsUrl,
|
||||
settings.ShowactsContent,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerEyebrow),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerText),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerLiveButtonLabel),
|
||||
settings.StreamBannerLiveButtonUrl,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerLockedButtonLabel),
|
||||
settings.StreamBannerUseCompletedContent,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedEyebrow),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedText),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedButtonLabel),
|
||||
settings.StreamBannerCompletedButtonUrl,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.AwardsSectionTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.AwardsSectionDescription),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.SubcategoriesSectionTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.SubcategoriesSectionDescription),
|
||||
SeasonMappings.ReadSocialLinks(settings),
|
||||
SeasonMappings.ReadFaqItems(settings),
|
||||
settings.ShowactFormSchemaJson ?? "[]"));
|
||||
return Results.Ok(MapSiteSettingsResponse(settings));
|
||||
}
|
||||
|
||||
private static async Task<IResult> UploadHostImage(
|
||||
HttpContext context,
|
||||
AwardsDbContext db,
|
||||
IAdminAuditService adminAuditService)
|
||||
{
|
||||
if (!context.Request.HasFormContentType)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Bitte ein Bild als Formular-Upload senden." });
|
||||
}
|
||||
|
||||
var form = await context.Request.ReadFormAsync(context.RequestAborted);
|
||||
var file = form.Files.GetFile("file") ?? form.Files.FirstOrDefault();
|
||||
if (file is null || file.Length == 0)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Bitte ein Hostbild auswählen." });
|
||||
}
|
||||
|
||||
if (file.Length > MaxHostImageBytes)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Hostbild ist zu groß. Maximal erlaubt sind 8 MB." });
|
||||
}
|
||||
|
||||
if (!AllowedHostImageContentTypes.TryGetValue(file.ContentType, out var expectedExtension))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Bitte PNG, JPG oder WebP hochladen." });
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(file.FileName);
|
||||
if (!string.IsNullOrWhiteSpace(extension)
|
||||
&& !string.Equals(extension, expectedExtension, StringComparison.OrdinalIgnoreCase)
|
||||
&& !(string.Equals(file.ContentType, "image/jpeg", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Dateiendung und Bildtyp passen nicht zusammen." });
|
||||
}
|
||||
|
||||
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
|
||||
if (settings is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
await using var stream = file.OpenReadStream();
|
||||
using var memory = new MemoryStream((int)file.Length);
|
||||
await stream.CopyToAsync(memory, context.RequestAborted);
|
||||
|
||||
settings.HostImageData = memory.ToArray();
|
||||
settings.HostImageContentType = file.ContentType;
|
||||
settings.HostImageUpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
var session = AdminEndpointConventions.CurrentSession(context);
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
"site-settings.host-image.upload",
|
||||
"site-settings",
|
||||
settings.Id.ToString(),
|
||||
"Landingpage-Hostbild wurde aktualisiert.",
|
||||
new
|
||||
{
|
||||
fileName = file.FileName,
|
||||
fileSize = file.Length,
|
||||
file.ContentType,
|
||||
},
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(MapSiteSettingsResponse(settings));
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateSiteSettings(
|
||||
@@ -129,6 +175,7 @@ public static class AdminSiteSettingsEndpoints
|
||||
|
||||
settings.HostDisplayName = request.HostDisplayName.Trim();
|
||||
settings.HostTagline = request.HostTagline.Trim();
|
||||
settings.HostArtistName = request.HostArtistName.Trim();
|
||||
settings.NewsletterUrl = normalizedUrls.NewsletterUrl;
|
||||
settings.ShareXUrl = normalizedUrls.ShareXUrl;
|
||||
settings.ShareDiscordUrl = normalizedUrls.ShareDiscordUrl;
|
||||
@@ -189,6 +236,61 @@ public static class AdminSiteSettingsEndpoints
|
||||
return Results.Ok(new { saved = true });
|
||||
}
|
||||
|
||||
private static AdminSiteSettingsResponse MapSiteSettingsResponse(SiteSettings settings)
|
||||
{
|
||||
return new AdminSiteSettingsResponse(
|
||||
settings.HostDisplayName,
|
||||
settings.HostTagline,
|
||||
settings.HostArtistName,
|
||||
BuildHostImageUrl(settings),
|
||||
settings.NewsletterUrl,
|
||||
settings.ShareXUrl,
|
||||
settings.ShareDiscordUrl,
|
||||
settings.PrivacyEmail,
|
||||
settings.PrivacyPolicyContent,
|
||||
settings.PrivacyPolicyUpdatedBy,
|
||||
settings.PrivacyPolicyUpdatedAt,
|
||||
settings.ImprintUrl,
|
||||
settings.ImprintContent,
|
||||
settings.ContactUrl,
|
||||
settings.ContactContent,
|
||||
settings.SponsorsUrl,
|
||||
settings.SponsorsContent,
|
||||
settings.ShowactsUrl,
|
||||
settings.ShowactsContent,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerEyebrow),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerText),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerLiveButtonLabel),
|
||||
settings.StreamBannerLiveButtonUrl,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerLockedButtonLabel),
|
||||
settings.StreamBannerUseCompletedContent,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedEyebrow),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedText),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedButtonLabel),
|
||||
settings.StreamBannerCompletedButtonUrl,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.AwardsSectionTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.AwardsSectionDescription),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.SubcategoriesSectionTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.SubcategoriesSectionDescription),
|
||||
SeasonMappings.ReadSocialLinks(settings),
|
||||
SeasonMappings.ReadFaqItems(settings),
|
||||
settings.ShowactFormSchemaJson ?? "[]");
|
||||
}
|
||||
|
||||
internal static string BuildHostImageUrl(SiteSettings settings)
|
||||
{
|
||||
if (settings.HostImageData is not { Length: > 0 })
|
||||
{
|
||||
return DefaultHostImageUrl;
|
||||
}
|
||||
|
||||
var version = settings.HostImageUpdatedAt?.ToUnixTimeSeconds().ToString(System.Globalization.CultureInfo.InvariantCulture)
|
||||
?? settings.HostImageData.Length.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
return $"/api/public/host-image?v={version}";
|
||||
}
|
||||
|
||||
private static IResult? NormalizeSiteSettingsUrls(
|
||||
UpdateSiteSettingsRequest request,
|
||||
out PublicSiteUrlSettings normalizedUrls,
|
||||
|
||||
Reference in New Issue
Block a user