Compare commits

...

16 Commits

Author SHA1 Message Date
AzuTear 23fddbd976 Publish frontend 0.2.3 release notes
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 1m26s
CI - Build & Verify / Deploy to award.noveria.net (push) Failing after 11m22s
2026-07-09 23:47:13 +02:00
AzuTear 353eb97563 Sync team role permissions after save 2026-07-09 23:47:07 +02:00
AzuTear 33ba00a682 Harden request validation and admin UX cues
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 58s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m24s
2026-06-30 08:45:28 +02:00
AzuTear 1191abc914 Refresh live demo public content
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 1m2s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m36s
2026-06-30 08:25:16 +02:00
AzuTear 76ac60d3fd Fix admin demo review and landing regressions
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 59s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m34s
2026-06-30 08:18:14 +02:00
AzuTear 0803a0124b Seed 2025 demo archive winners despite empty real season shells
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 1m2s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m31s
The prior SeedDemoArchivedWinners2025 seed skipped whenever any non-demo
2025 season row existed. Production carries empty 2025/2024 season shells
with no published winners, so the demo archive stayed empty and
overview archiveYears returned []. Re-seed with a precise guard that only
skips when genuine published 2025 winner results exist; idempotent via
ON CONFLICT DO NOTHING.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:56:10 +02:00
AzuTear 6b3b0360e7 Add winner archive, host image upload and live demo data
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 1m1s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m30s
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>
2026-06-29 21:47:52 +02:00
AzuTear f323e5a82c Seed current season demo voting data
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 56s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m25s
2026-06-29 18:47:00 +02:00
AzuTear 09dc1de451 Seed live demo award data
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 57s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m27s
2026-06-29 18:41:41 +02:00
AzuTear 049db2e0e0 Verify deployed build metadata
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 1m1s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 3m39s
2026-06-29 18:23:33 +02:00
AzuTear 65c9815baa Guard demo seed migrations
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 59s
CI - Build & Verify / Deploy to award.noveria.net (push) Failing after 1m27s
2026-06-29 18:19:18 +02:00
AzuTear 3794b7f29e Free deploy disk headroom before migrations
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 55s
CI - Build & Verify / Deploy to award.noveria.net (push) Failing after 1m29s
2026-06-29 18:13:19 +02:00
AzuTear 709a024820 Ensure demo seed schema
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 55s
CI - Build & Verify / Deploy to award.noveria.net (push) Failing after 54s
2026-06-29 18:10:02 +02:00
AzuTear e497f19652 Ensure site settings seed columns
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 59s
CI - Build & Verify / Deploy to award.noveria.net (push) Failing after 54s
2026-06-29 18:06:34 +02:00
AzuTear c9fd3e47cb Add landing content settings migration
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 57s
CI - Build & Verify / Deploy to award.noveria.net (push) Failing after 57s
2026-06-29 18:02:05 +02:00
AzuTear 1a74c03621 Restore production migration history
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 57s
CI - Build & Verify / Deploy to award.noveria.net (push) Failing after 56s
2026-06-29 17:57:47 +02:00
180 changed files with 52404 additions and 1498 deletions
+35 -2
View File
@@ -171,9 +171,40 @@ jobs:
alpine:latest \
sh -lc '
test -d /app/frontend
mkdir -p /app/frontend/public
printf "VITE_BUILD_VERSION=%s\nVITE_BUILD_DATE=%s\n" "$BUILD_VERSION" "$BUILD_DATE" > /app/frontend/.env.production
printf "{\n \"version\": \"%s\",\n \"buildDate\": \"%s\"\n}\n" "$BUILD_VERSION" "$BUILD_DATE" > /app/frontend/public/build-meta.json
echo "Frontend build metadata:"
cat /app/frontend/.env.production
cat /app/frontend/public/build-meta.json
'
- name: Prepare deploy host disk headroom
run: |
set -euo pipefail
docker run --rm \
-v "${DEPLOY_PATH}:/workspace/vtube-awards" \
-v /var/run/docker.sock:/var/run/docker.sock \
-w /workspace/vtube-awards \
docker:cli \
sh -lc '
set -e
echo "Disk before cleanup:"
df -h .
mkdir -p backups
old_backups="$(ls -1t backups/predeploy-*.dump 2>/dev/null | tail -n +4 || true)"
if [ -n "$old_backups" ]; then
echo "$old_backups" | while IFS= read -r backup; do
[ -n "$backup" ] && rm -f "$backup"
done
fi
docker builder prune -af || true
docker image prune -af || true
echo "Disk after cleanup:"
df -h .
'
- name: Build Docker images
@@ -280,9 +311,11 @@ jobs:
css_asset="$(printf "%s" "$index" | grep -Eo '/assets/index-[^"]+\.css' | head -n 1)"
test -n "$js_asset"
test -n "$css_asset"
js_body="$(curl -fsS --max-time 20 "${LIVE_URL}${js_asset}")"
printf "%s" "$js_body" | grep -Fq "${{ steps.meta.outputs.build_version }}"
curl -fsS --max-time 20 "${LIVE_URL}${js_asset}" >/dev/null
curl -fsS --max-time 20 "${LIVE_URL}${css_asset}" >/dev/null
meta_body="$(curl -fsS --max-time 15 "${LIVE_URL}/build-meta.json")"
echo "$meta_body"
echo "$meta_body" | grep -Fq '"version": "${{ steps.meta.outputs.build_version }}"'
echo "Frontend assets served: ${js_asset}, ${css_asset}"
echo "Frontend version verified: ${{ steps.meta.outputs.build_version }}"
+44
View File
@@ -92,6 +92,50 @@ public static class SeasonMappings
};
}
public static (string Platform, string Slug) InferProfileMetadataFromUrl(string? value, string fallbackName = "")
{
var trimmed = value?.Trim() ?? string.Empty;
if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var uri))
{
return ("Profil", fallbackName.Trim());
}
var host = uri.Host.Trim().ToLowerInvariant();
var segments = uri.AbsolutePath
.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var platform = host switch
{
var item when item.Contains("twitch.tv", StringComparison.Ordinal) => "Twitch",
var item when item.Contains("youtube.com", StringComparison.Ordinal) || item.Contains("youtu.be", StringComparison.Ordinal) => "YouTube",
var item when item.Contains("x.com", StringComparison.Ordinal) || item.Contains("twitter.com", StringComparison.Ordinal) => "X",
var item when item.Contains("instagram.com", StringComparison.Ordinal) => "Instagram",
var item when item.Contains("discord.gg", StringComparison.Ordinal) || item.Contains("discord.com", StringComparison.Ordinal) => "Discord",
var item when item.Contains("kick.com", StringComparison.Ordinal) => "Kick",
var item when item.Contains("cake.gg", StringComparison.Ordinal) => "Cake",
_ => "Profil",
};
var slug = segments.LastOrDefault() ?? string.Empty;
if (string.Equals(platform, "YouTube", StringComparison.Ordinal) && segments.Length > 0)
{
slug = segments.FirstOrDefault(segment => segment.StartsWith('@')) ?? slug;
}
slug = Uri.UnescapeDataString(slug).Trim().Trim('/');
if (slug.StartsWith('@') && !string.Equals(platform, "YouTube", StringComparison.Ordinal))
{
slug = slug[1..];
}
if (string.IsNullOrWhiteSpace(slug))
{
slug = fallbackName.Trim();
}
return (platform, slug);
}
public static string NormalizeSeasonStreamUrl(string? value)
{
var trimmed = value?.Trim() ?? string.Empty;
@@ -0,0 +1,18 @@
namespace Backend.Contracts;
public sealed record AdminArchivedWinnerItemDto(
int Id,
int Year,
string Category,
string Subcategory,
string WinnerName,
string WinnerUrl,
DateTimeOffset CreatedAt,
DateTimeOffset? UpdatedAt);
public sealed record UpsertArchivedWinnerRequest(
int Year,
string Category,
string Subcategory,
string WinnerName,
string WinnerUrl);
@@ -37,6 +37,8 @@ public sealed record AdminCandidateItemDto(
string DisplayName,
string ChannelSlug,
string Platform,
int? AvgViewers,
int Votes,
int NominationTally,
string AcceptanceStatus,
string? AcceptanceNote,
@@ -3,6 +3,8 @@ namespace Backend.Contracts;
public sealed record AdminSiteSettingsResponse(
string HostDisplayName,
string HostTagline,
string HostArtistName,
string HostImageUrl,
string NewsletterUrl,
string ShareXUrl,
string ShareDiscordUrl,
@@ -41,6 +43,7 @@ public sealed record AdminSiteSettingsResponse(
public sealed record UpdateSiteSettingsRequest(
string HostDisplayName,
string HostTagline,
string HostArtistName,
string NewsletterUrl,
string ShareXUrl,
string ShareDiscordUrl,
@@ -64,6 +64,8 @@ public sealed record PublicStreamBannerContentDto(
public sealed record PublicSiteContentDto(
string HostDisplayName,
string HostTagline,
string HostArtistName,
string HostImageUrl,
string NewsletterUrl,
string ShareXUrl,
string ShareDiscordUrl,
+13
View File
@@ -10,6 +10,7 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
public DbSet<Candidate> Candidates => Set<Candidate>();
public DbSet<StreamerIdentity> StreamerIdentities => Set<StreamerIdentity>();
public DbSet<AwardResult> Results => Set<AwardResult>();
public DbSet<ArchivedWinner> ArchivedWinners => Set<ArchivedWinner>();
public DbSet<Nomination> Nominations => Set<Nomination>();
public DbSet<VoteBallot> VoteBallots => Set<VoteBallot>();
public DbSet<VoteEntry> VoteEntries => Set<VoteEntry>();
@@ -40,6 +41,9 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
{
entity.Property(item => item.HostDisplayName).HasMaxLength(120);
entity.Property(item => item.HostTagline).HasMaxLength(160);
entity.Property(item => item.HostArtistName).HasMaxLength(120);
entity.Property(item => item.HostImageData).HasColumnType("bytea");
entity.Property(item => item.HostImageContentType).HasMaxLength(80);
entity.Property(item => item.NewsletterUrl).HasMaxLength(400);
entity.Property(item => item.PrivacyEmail).HasMaxLength(160);
entity.Property(item => item.PrivacyPolicyUpdatedBy).HasMaxLength(120);
@@ -186,6 +190,15 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
entity.Property(item => item.CategoryName).HasMaxLength(120);
});
modelBuilder.Entity<ArchivedWinner>(entity =>
{
entity.HasIndex(item => new { item.Year, item.Category, item.Subcategory }).IsUnique();
entity.Property(item => item.Category).HasMaxLength(120);
entity.Property(item => item.Subcategory).HasMaxLength(120);
entity.Property(item => item.WinnerName).HasMaxLength(120);
entity.Property(item => item.WinnerUrl).HasMaxLength(500);
});
modelBuilder.Entity<UserSession>(entity =>
{
entity.HasIndex(item => item.SessionToken).IsUnique();
+184
View File
@@ -0,0 +1,184 @@
namespace Backend.Data;
using Backend.Services;
internal sealed record AwardCategorySeed(string LegacyGroupName, string Name, string Slug, string Description, int SortOrder);
internal sealed record CandidateSeed(string CategorySlug, string DisplayName, string ChannelSlug, string Platform);
internal sealed record WinnerSeed(int Year, string CategorySlug, string DisplayName, string ChannelSlug, string Platform);
internal sealed record SiteFaqSeed(string Question, string Answer);
internal sealed record SiteSocialSeed(string Label, string Platform, string Url, string Icon);
internal sealed record SponsorSeed(string Name, string WebsiteUrl, string LogoUrl, string Description, string Tier, int SortOrder);
internal static class SeedCatalog
{
internal static readonly SeasonSubcategoryTemplateSetting[] DefaultSubcategoryTemplates =
[
new("Hidden Star", "hidden-star", 1, 1, 20),
new("Rising Star", "rising-star", 2, 21, 60),
new("Shining Star", "shining-star", 3, 61, null),
];
internal static readonly AwardCategorySeed[] AwardCategorySeeds =
[
new("Main Awards", "VTuber des Jahres", "vtuber-des-jahres", "Die größte Auszeichnung des Jahres.", 1),
new("Discovery", "Best Newcomer", "best-newcomer", "Neue Stimmen, neue Welten und frische Energie für die Szene.", 2),
new("Creative", "Model & Design", "model-design", "Live2D, 3D, Outfit, Rigging und visuelle Identitaet.", 3),
new("Performance", "Gesang & Musik", "gesang-musik", "Songs, Covers, Konzerte und musikalische Highlights.", 4),
new("Gaming", "Best Gaming", "best-gaming", "Gameplay, Skill, Chaos und legendaere Gaming-Momente.", 5),
new("Entertainment", "Best Variety", "best-variety", "Talk, Comedy, Watchalongs und kreative Streamformate.", 6),
new("Community", "Community Liebling", "community-liebling", "Creator:innen, die ihre Community besonders stark verbinden.", 7),
new("Collab", "Best Collab & Duo", "best-collab-duo", "Gemeinsame Streams, Projekte und Duo-Dynamik.", 8),
];
internal static readonly Dictionary<string, string> LegacyCategorySlugMap = new(StringComparer.OrdinalIgnoreCase)
{
["bestes-live-event"] = "best-newcomer",
["clip-des-jahres"] = "model-design",
["beste-community"] = "gesang-musik",
};
internal static readonly SiteFaqSeed[] SiteFaqSeeds =
[
new(
"Wer darf nominiert werden?",
"Jede:r aktive deutschsprachige VTuber kann nominiert werden — unabhaengig von Follower-Zahl oder Plattform. Die Community schlaegt in der Nominierungsphase ihre Favorit:innen vor."),
new(
"Wie funktioniert das Voting?",
"Du meldest dich ausschließlich mit deinem Twitch-Account an — nur so kannst du teilnehmen. Das hält das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, änderbar bis zum Ende der Phase."),
new(
"Was kostet die Teilnahme?",
"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos — der VTuber Star Award ist ein Community-Event von Fans für Fans."),
new(
"Wann und wo findet die Award-Show statt?",
"Die große Live-Show wird von Jayuhime gehostet und auf Twitch & YouTube gestreamt. Den genauen Termin findest du im Countdown oben — sei live dabei, wenn die Stars gekürt werden!"),
new(
"Ich wurde nominiert — was nun?",
"Glückwunsch! Du erhältst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf — jede Stimme zählt."),
];
internal static readonly SiteSocialSeed[] SiteSocialSeeds =
[
new("Twitch", "twitch", "https://twitch.tv/jayuhime", "twitch"),
new("YouTube", "youtube", "https://youtube.com/c/Jayuhime", "youtube"),
new("X", "x", "https://x.com/jayuhime", "x"),
new("Instagram", "instagram", "https://instagram.com/jayuhime", "instagram"),
new("Discord", "discord", "https://discord.gg/jayuhime", "discord"),
];
internal const string DefaultImprintContent = """
Anbieter
VTuber Star Awards, vertreten durch Jayuhime.
Kontakt
Nutze für organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.
Hinweis
Dieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsächlichen Anbieterangaben ersetzt werden.
""";
internal const string DefaultContactContent = """
Kontakt zum Award-Team
Du hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team über die hinterlegte Kontaktseite.
Datenschutzfragen
Für Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.
Community & Kooperationen
Social Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.
""";
internal const string DefaultSponsorsContent = """
Sponsoren & Partner
Hier können Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.
Partner werden im Rahmen der Show und auf den öffentlichen Kontaktflächen genannt, sobald sie final bestätigt sind.
""";
internal static readonly CandidateSeed[] CurrentCandidateSeeds =
[
new("vtuber-des-jahres-shining-star", "Hoshimi Miyu", "@hoshimimiyu", "Twitch"),
new("vtuber-des-jahres-shining-star", "Kurainu", "@kurainu", "Twitch"),
new("vtuber-des-jahres-shining-star", "Shiro Ch.", "@shiroch", "Twitch"),
new("best-newcomer-hidden-star", "Aoi Sakura", "@aoisakura", "YouTube"),
new("best-newcomer-rising-star", "Nox Live", "@noxlive", "Twitch"),
new("model-design-shining-star", "Velvet Rei", "@velvetrei", "Twitch"),
new("model-design-rising-star", "Mochi Atelier", "@mochiatelier", "Cake"),
new("gesang-musik-shining-star", "Melo Diva", "@melodiva", "YouTube"),
new("gesang-musik-rising-star", "Yuki Stern", "@yukistern", "Twitch"),
new("best-gaming-shining-star", "Kurainu", "@kurainu", "Twitch"),
new("best-gaming-rising-star", "PixelPunk", "@pixelpunk", "Twitch"),
new("best-variety-shining-star", "Taro Chaos", "@tarochaos", "Twitch"),
new("best-variety-rising-star", "Kotaro Plays", "@kotaroplays", "YouTube"),
new("community-liebling-shining-star", "Shiro Ch.", "@shiroch", "Twitch"),
new("community-liebling-hidden-star", "Lumi", "@lumi_vt", "Cake"),
new("best-collab-duo-shining-star", "Akari & Nox", "@akari_vt", "Twitch"),
new("best-collab-duo-rising-star", "Mochi & Hana", "@mochi_mochi", "YouTube"),
];
internal static readonly WinnerSeed[] WinnerSeeds =
[
new(2025, "vtuber-des-jahres-shining-star", "Hoshimi Miyu", "@hoshimimiyu", "Twitch"),
new(2025, "best-newcomer-hidden-star", "Aoi Sakura", "@aoisakura", "YouTube"),
new(2025, "model-design-shining-star", "Velvet Rei", "@velvetrei", "Twitch"),
new(2025, "gesang-musik-shining-star", "Melo Diva", "@melodiva", "YouTube"),
new(2025, "best-gaming-shining-star", "Kurainu", "@kurainu", "Twitch"),
new(2025, "best-variety-shining-star", "Taro Chaos", "@tarochaos", "Twitch"),
new(2025, "community-liebling-shining-star", "Shiro Ch.", "@shiroch", "Twitch"),
new(2025, "best-collab-duo-shining-star", "Akari & Nox", "@akari_vt", "Cake"),
new(2024, "vtuber-des-jahres-shining-star", "Aoi Sakura", "@aoisakura", "YouTube"),
new(2024, "best-newcomer-hidden-star", "Lumi", "@lumi_vt", "Cake"),
new(2024, "model-design-rising-star", "Mochi Atelier", "@mochiatelier", "Cake"),
new(2024, "gesang-musik-rising-star", "Yuki Stern", "@yukistern", "Twitch"),
new(2024, "best-gaming-shining-star", "Starbyte", "@starbyte", "Twitch"),
new(2024, "best-variety-rising-star", "Kotaro Plays", "@kotaroplays", "YouTube"),
new(2024, "community-liebling-rising-star", "Moonrelay", "@moonrelay", "Twitch"),
new(2024, "best-collab-duo-rising-star", "Pixel & Kotaro", "@pixelpunk", "Twitch"),
new(2023, "vtuber-des-jahres-shining-star", "Akari Nova", "@akarinova", "Twitch"),
new(2023, "best-newcomer-rising-star", "Nox Live", "@noxlive", "Twitch"),
new(2023, "model-design-shining-star", "Rei Velvet", "@reivelvet", "YouTube"),
new(2023, "gesang-musik-shining-star", "Tenshi Vox", "@tenshivox", "Twitch"),
new(2023, "best-gaming-rising-star", "Bit Knight", "@bitknight", "Twitch"),
new(2023, "best-variety-hidden-star", "Hana Hearts", "@hanahearts", "Cake"),
new(2023, "community-liebling-rising-star", "Sora Blau", "@sorablau", "YouTube"),
new(2023, "best-collab-duo-rising-star", "Yuki & Melo", "@yukistern", "Twitch"),
];
internal static readonly SponsorSeed[] DemoSponsorSeeds =
[
new(
"HoshiForge Studio",
"https://hoshiforge.example",
"/demo/sponsors/hoshiforge-studio.svg",
"Branding-, Overlay- und Debuet-Visuals fuer VTuber-Projekte und Community-Events.",
"Presenting Sponsor",
10),
new(
"NekoPixel Energy",
"https://nekopixel.example",
"/demo/sponsors/nekopixel-energy.svg",
"Community-fokussierter Drink-Partner fuer lange Showabende, Watchpartys und Creator-Collabs.",
"Gold Partner",
20),
new(
"PrismLoop Audio",
"https://prismloop.example",
"/demo/sponsors/prismloop-audio.svg",
"Audio-Tools, Intro-Packs und Stream-Sounddesign fuer Live-Shows und Highlight-Clips.",
"Gold Partner",
30),
new(
"CloudBeacon Hosting",
"https://cloudbeacon.example",
"/demo/sponsors/cloudbeacon-hosting.svg",
"Skalierbares Hosting fuer Voting, Landingpages und Event-Traffic rund um Showtage.",
"Tech Partner",
40),
new(
"ChibiCanvas Market",
"https://chibicanvas.example",
"/demo/sponsors/chibicanvas-market.svg",
"Merch-, Sticker- und Artist-Marketplace mit Fokus auf VTuber, Emotes und Fanartikel.",
"Community Partner",
50),
];
}
+13
View File
@@ -0,0 +1,13 @@
namespace Backend.Domain;
public sealed class ArchivedWinner
{
public int Id { get; set; }
public int Year { get; set; }
public string Category { get; set; } = string.Empty;
public string Subcategory { get; set; } = string.Empty;
public string WinnerName { get; set; } = string.Empty;
public string WinnerUrl { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? UpdatedAt { get; set; }
}
+4
View File
@@ -5,6 +5,10 @@ public sealed class SiteSettings
public int Id { get; set; }
public string HostDisplayName { get; set; } = string.Empty;
public string HostTagline { get; set; } = string.Empty;
public string HostArtistName { get; set; } = string.Empty;
public byte[]? HostImageData { get; set; }
public string? HostImageContentType { get; set; }
public DateTimeOffset? HostImageUpdatedAt { get; set; }
public string NewsletterUrl { get; set; } = string.Empty;
public string ShareXUrl { get; set; } = string.Empty;
public string ShareDiscordUrl { get; set; } = string.Empty;
+249
View File
@@ -0,0 +1,249 @@
using Backend.Common;
using Backend.Contracts;
using Backend.Data;
using Backend.Domain;
using Backend.Security;
using Backend.Services;
using Microsoft.EntityFrameworkCore;
namespace Backend.Endpoints;
public static class AdminArchiveEndpoints
{
public static RouteGroupBuilder MapAdminArchiveEndpoints(this RouteGroupBuilder group)
{
group.MapGet("/archived-winners", GetArchivedWinners)
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequirePermission(context, next, AdminPermissionCatalog.Winners))
.WithName("GetAdminArchivedWinners");
group.MapPost("/archived-winners", CreateArchivedWinner)
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Winners))
.WithName("CreateAdminArchivedWinner");
group.MapPut("/archived-winners/{archivedWinnerId:int}", UpdateArchivedWinner)
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Winners))
.WithName("UpdateAdminArchivedWinner");
group.MapDelete("/archived-winners/{archivedWinnerId:int}", DeleteArchivedWinner)
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Winners))
.WithName("DeleteAdminArchivedWinner");
return group;
}
private static async Task<IResult> GetArchivedWinners(AwardsDbContext db, CancellationToken cancellationToken)
{
var items = await db.ArchivedWinners
.AsNoTracking()
.OrderByDescending(item => item.Year)
.ThenBy(item => item.Category)
.ThenBy(item => item.Subcategory)
.ThenBy(item => item.WinnerName)
.Select(item => ToDto(item))
.ToArrayAsync(cancellationToken);
return Results.Ok(items);
}
private static async Task<IResult> CreateArchivedWinner(
HttpContext context,
UpsertArchivedWinnerRequest request,
AwardsDbContext db,
IAdminAuditService adminAuditService)
{
var session = AdminEndpointConventions.CurrentSession(context);
var validation = ValidateRequest(request);
if (validation is not null)
{
return validation;
}
var normalized = NormalizeRequest(request);
var duplicateExists = await db.ArchivedWinners.AnyAsync(item =>
item.Year == normalized.Year
&& item.Category == normalized.Category
&& item.Subcategory == normalized.Subcategory,
context.RequestAborted);
if (duplicateExists)
{
return Results.BadRequest(new { message = "Für dieses Jahr, diese Kategorie und Unterkategorie existiert bereits ein Archivgewinner." });
}
var archivedWinner = new ArchivedWinner
{
Year = normalized.Year,
Category = normalized.Category,
Subcategory = normalized.Subcategory,
WinnerName = normalized.WinnerName,
WinnerUrl = normalized.WinnerUrl,
};
db.ArchivedWinners.Add(archivedWinner);
adminAuditService.AddEntry(
session.TwitchUserId,
"archived-winner.create",
"archivedWinner",
$"{normalized.Year}:{normalized.Category}:{normalized.Subcategory}",
$"Archivgewinner {normalized.Year} · {normalized.Category} · {normalized.Subcategory} angelegt.",
new
{
normalized.Year,
normalized.Category,
normalized.Subcategory,
normalized.WinnerName,
normalized.WinnerUrl,
},
RequestMetadataReader.Read(context));
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new { saved = true, entry = ToDto(archivedWinner) });
}
private static async Task<IResult> UpdateArchivedWinner(
HttpContext context,
int archivedWinnerId,
UpsertArchivedWinnerRequest request,
AwardsDbContext db,
IAdminAuditService adminAuditService)
{
var session = AdminEndpointConventions.CurrentSession(context);
var validation = ValidateRequest(request);
if (validation is not null)
{
return validation;
}
var archivedWinner = await db.ArchivedWinners.FirstOrDefaultAsync(item => item.Id == archivedWinnerId, context.RequestAborted);
if (archivedWinner is null)
{
return Results.NotFound();
}
var normalized = NormalizeRequest(request);
var duplicateExists = await db.ArchivedWinners.AnyAsync(item =>
item.Id != archivedWinnerId
&& item.Year == normalized.Year
&& item.Category == normalized.Category
&& item.Subcategory == normalized.Subcategory,
context.RequestAborted);
if (duplicateExists)
{
return Results.BadRequest(new { message = "Für dieses Jahr, diese Kategorie und Unterkategorie existiert bereits ein Archivgewinner." });
}
archivedWinner.Year = normalized.Year;
archivedWinner.Category = normalized.Category;
archivedWinner.Subcategory = normalized.Subcategory;
archivedWinner.WinnerName = normalized.WinnerName;
archivedWinner.WinnerUrl = normalized.WinnerUrl;
archivedWinner.UpdatedAt = DateTimeOffset.UtcNow;
adminAuditService.AddEntry(
session.TwitchUserId,
"archived-winner.update",
"archivedWinner",
archivedWinner.Id.ToString(),
$"Archivgewinner {normalized.Year} · {normalized.Category} · {normalized.Subcategory} aktualisiert.",
new
{
archivedWinner.Id,
normalized.Year,
normalized.Category,
normalized.Subcategory,
normalized.WinnerName,
normalized.WinnerUrl,
},
RequestMetadataReader.Read(context));
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new { saved = true, entry = ToDto(archivedWinner) });
}
private static async Task<IResult> DeleteArchivedWinner(
HttpContext context,
int archivedWinnerId,
AwardsDbContext db,
IAdminAuditService adminAuditService)
{
var session = AdminEndpointConventions.CurrentSession(context);
var archivedWinner = await db.ArchivedWinners.FirstOrDefaultAsync(item => item.Id == archivedWinnerId, context.RequestAborted);
if (archivedWinner is null)
{
return Results.NotFound();
}
db.ArchivedWinners.Remove(archivedWinner);
adminAuditService.AddEntry(
session.TwitchUserId,
"archived-winner.delete",
"archivedWinner",
archivedWinner.Id.ToString(),
$"Archivgewinner {archivedWinner.Year} · {archivedWinner.Category} · {archivedWinner.Subcategory} gelöscht.",
new
{
archivedWinner.Id,
archivedWinner.Year,
archivedWinner.Category,
archivedWinner.Subcategory,
archivedWinner.WinnerName,
archivedWinner.WinnerUrl,
},
RequestMetadataReader.Read(context));
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new { deleted = true, archivedWinnerId });
}
private static AdminArchivedWinnerItemDto ToDto(ArchivedWinner item) =>
new(
item.Id,
item.Year,
item.Category,
item.Subcategory,
item.WinnerName,
item.WinnerUrl,
item.CreatedAt,
item.UpdatedAt);
private static IResult? ValidateRequest(UpsertArchivedWinnerRequest request)
{
if (request.Year < 2000 || request.Year > 3000)
{
return Results.BadRequest(new { message = "Bitte ein gültiges Archivjahr angeben." });
}
if (string.IsNullOrWhiteSpace(request.Category))
{
return Results.BadRequest(new { message = "Die Kategorie darf nicht leer sein." });
}
if (string.IsNullOrWhiteSpace(request.Subcategory))
{
return Results.BadRequest(new { message = "Die Unterkategorie darf nicht leer sein." });
}
if (string.IsNullOrWhiteSpace(request.WinnerName))
{
return Results.BadRequest(new { message = "Der Gewinnername darf nicht leer sein." });
}
var winnerUrl = request.WinnerUrl?.Trim() ?? string.Empty;
if (!Uri.TryCreate(winnerUrl, UriKind.Absolute, out var uri)
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
{
return Results.BadRequest(new { message = "Bitte einen gültigen http- oder https-Link angeben." });
}
return null;
}
private static (int Year, string Category, string Subcategory, string WinnerName, string WinnerUrl) NormalizeRequest(UpsertArchivedWinnerRequest request) =>
(
request.Year,
SeasonMappings.NormalizePlainTextContent(request.Category),
SeasonMappings.NormalizePlainTextContent(request.Subcategory),
SeasonMappings.NormalizePlainTextContent(request.WinnerName),
request.WinnerUrl.Trim());
}
+1
View File
@@ -13,6 +13,7 @@ public static class AdminEndpoints
group.MapAdminDashboardEndpoints();
group.MapAdminSeasonManagementEndpoints();
group.MapAdminArchiveEndpoints();
group.MapAdminModerationEndpoints();
group.MapAdminExtrasEndpoints();
group.MapAdminTeamEndpoints();
@@ -28,13 +28,6 @@ public static partial class AdminModerationEndpoints
return Results.NotFound();
}
var trackingFlags = TrackingRulesSettings.ReadFlagHits(nomination.TrackingFlagsJson);
if (trackingFlags.Any(flag => flag.BlocksApproval)
&& !string.Equals(nomination.TrackingReviewStatus, "overridden", StringComparison.OrdinalIgnoreCase))
{
return Results.BadRequest(new { message = "Tracking Rules blockieren die Freigabe. Bitte setze zuerst einen manuellen Override im Review." });
}
var rawDisplayName = FirstNonEmpty(request.DisplayName, nomination.CandidateText, nomination.ResolvedChannel);
if (string.IsNullOrWhiteSpace(rawDisplayName))
+7 -2
View File
@@ -151,10 +151,15 @@ public static partial class AdminModerationEndpoints
private static async Task<IResult> BulkResolveRiskFlags(
HttpContext context,
BulkResolveRiskFlagsRequest request,
BulkResolveRiskFlagsRequest? request,
AwardsDbContext db,
IAdminAuditService adminAuditService)
{
if (request is null || request.RiskFlagIds is null)
{
return Results.BadRequest(new { message = "Bulk-Verarbeitung braucht mindestens einen Risikohinweis." });
}
var session = AdminEndpointConventions.CurrentSession(context);
var normalizedIds = request.RiskFlagIds
.Distinct()
@@ -188,7 +193,7 @@ public static partial class AdminModerationEndpoints
if (riskFlags.Any(item => item.Severity != "low"))
{
return Results.BadRequest(new { message = "Bulk-Verarbeitung ist nur fuer Low-Severity-Hinweise erlaubt." });
return Results.BadRequest(new { message = "Bulk-Verarbeitung ist nur fuer offene Low-Severity-Hinweise erlaubt." });
}
if (riskFlags.Any(item => item.Status != "open"))
@@ -193,6 +193,34 @@ public static partial class AdminSeasonManagementEndpoints
return Results.Ok(new { saved = true, candidateId = candidate.Id });
}
private static async Task<IResult> GetCandidateDeletePreview(
int candidateId,
AwardsDbContext db,
CancellationToken cancellationToken)
{
var candidate = await db.Candidates
.AsNoTracking()
.FirstOrDefaultAsync(item => item.Id == candidateId, cancellationToken);
if (candidate is null)
{
return Results.NotFound();
}
var nominationCount = await db.Nominations.CountAsync(item => item.CandidateId == candidateId, cancellationToken);
var clipCount = await db.ClipSubmissions.CountAsync(item => item.CandidateId == candidateId, cancellationToken);
var voteCount = await db.VoteEntries.CountAsync(item => item.CandidateId == candidateId, cancellationToken);
var resultCount = await db.Results.CountAsync(item => item.CandidateId == candidateId, cancellationToken);
return Results.Ok(new
{
candidateId,
nominationCount,
clipCount,
voteCount,
resultCount,
});
}
private static async Task<IResult> DeleteCandidate(
HttpContext context,
int candidateId,
@@ -206,6 +234,38 @@ public static partial class AdminSeasonManagementEndpoints
return Results.NotFound();
}
var linkedNominations = await db.Nominations
.Where(item => item.CandidateId == candidateId)
.ToListAsync(context.RequestAborted);
if (linkedNominations.Count > 0)
{
db.Nominations.RemoveRange(linkedNominations);
}
var linkedClips = await db.ClipSubmissions
.Where(item => item.CandidateId == candidateId)
.ToListAsync(context.RequestAborted);
if (linkedClips.Count > 0)
{
db.ClipSubmissions.RemoveRange(linkedClips);
}
var linkedVoteEntries = await db.VoteEntries
.Where(item => item.CandidateId == candidateId)
.ToListAsync(context.RequestAborted);
if (linkedVoteEntries.Count > 0)
{
db.VoteEntries.RemoveRange(linkedVoteEntries);
}
var linkedResults = await db.Results
.Where(item => item.CandidateId == candidateId)
.ToListAsync(context.RequestAborted);
if (linkedResults.Count > 0)
{
db.Results.RemoveRange(linkedResults);
}
db.Candidates.Remove(candidate);
adminAuditService.AddEntry(
session.TwitchUserId,
@@ -213,11 +273,29 @@ public static partial class AdminSeasonManagementEndpoints
"candidate",
candidate.Id.ToString(),
$"Kandidat {candidate.DisplayName} wurde gelöscht.",
new { candidate.CategoryId, candidate.Platform },
new
{
candidate.CategoryId,
candidate.Platform,
deletedNominations = linkedNominations.Count,
deletedClips = linkedClips.Count,
deletedVoteEntries = linkedVoteEntries.Count,
deletedResults = linkedResults.Count,
},
RequestMetadataReader.Read(context));
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new { deleted = true, candidateId });
try
{
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new { deleted = true, candidateId });
}
catch (DbUpdateException)
{
return Results.BadRequest(new
{
message = "Kandidat konnte nicht gelöscht werden, weil noch verknüpfte Daten blockieren.",
});
}
}
private static string NormalizeCandidateChoice(string? value, string fallback, IReadOnlyCollection<string> allowedValues)
+97 -18
View File
@@ -23,11 +23,11 @@ public static partial class AdminSeasonManagementEndpoints
.FirstOrDefaultAsync(item => item.Id == 1);
var trackingRules = TrackingRulesSettings.Read(settings);
var candidates = await db.Candidates
var candidateRows = await db.Candidates
.AsNoTracking()
.Where(item => item.SeasonId == seasonId)
.OrderBy(item => item.DisplayName)
.Select(item => new AdminCandidateItemDto(
.Select(item => new AdminCandidateRow(
item.Id,
item.CategoryId,
item.StreamerIdentityId,
@@ -43,7 +43,7 @@ public static partial class AdminSeasonManagementEndpoints
item.ClipEmbedStatus))
.ToArrayAsync();
var candidateCounts = candidates
var candidateCounts = candidateRows
.GroupBy(item => item.CategoryId)
.ToDictionary(grouping => grouping.Key, grouping => grouping.Count());
@@ -76,21 +76,7 @@ public static partial class AdminSeasonManagementEndpoints
ViewerRangeMin = category.ViewerRangeMin,
ViewerRangeMax = category.ViewerRangeMax,
}));
var visibleCategoryRows = categoryRows
.Where(category => SeasonSubcategoryTemplateSettings.MatchesTemplate(
new Backend.Domain.Category
{
GroupName = category.GroupName,
Name = category.Name,
Slug = category.Slug,
SortOrder = category.SortOrder,
ViewerRangeMin = category.ViewerRangeMin,
ViewerRangeMax = category.ViewerRangeMax,
},
subcategoryTemplateSettings))
.ToArray();
var categories = visibleCategoryRows
var categories = categoryRows
.Select(category => new AdminCategoryItemDto(
category.Id,
category.GroupName,
@@ -217,6 +203,11 @@ public static partial class AdminSeasonManagementEndpoints
item.CategoryId,
item.CandidateId))
.ToArrayAsync();
var candidates = BuildCandidateItems(
candidateRows,
pendingNominationRows,
reviewedNominationRows,
votingEntryRows);
var votingWorkspace = BuildVotingWorkspace(
categories,
candidates,
@@ -281,6 +272,21 @@ public static partial class AdminSeasonManagementEndpoints
int CategoryId,
int CandidateId);
private sealed record AdminCandidateRow(
int Id,
int CategoryId,
int? StreamerIdentityId,
string DisplayName,
string ChannelSlug,
string Platform,
int NominationTally,
string AcceptanceStatus,
string? AcceptanceNote,
string? ClipCompilationUrl,
string? ClipCompilationTitle,
string? ClipCompilationPlatform,
string ClipEmbedStatus);
private sealed record AdminNominationRow(
int Id,
int? CategoryId,
@@ -314,6 +320,79 @@ public static partial class AdminSeasonManagementEndpoints
string? ReviewedByTwitchId,
DateTimeOffset? ReviewedAt);
private static AdminCandidateItemDto[] BuildCandidateItems(
AdminCandidateRow[] candidateRows,
AdminNominationRow[] pendingNominationRows,
AdminNominationRow[] reviewedNominationRows,
AdminVotingEntryRow[] votingEntryRows)
{
var allNominationRows = pendingNominationRows
.Concat(reviewedNominationRows)
.ToArray();
var voteCountByCandidate = votingEntryRows
.GroupBy(item => item.CandidateId)
.ToDictionary(group => group.Key, group => group.Count());
return candidateRows
.Select(item => new AdminCandidateItemDto(
item.Id,
item.CategoryId,
item.StreamerIdentityId,
item.DisplayName,
item.ChannelSlug,
item.Platform,
ResolveCandidateAvgViewers(item, allNominationRows),
voteCountByCandidate.GetValueOrDefault(item.Id, 0),
item.NominationTally,
item.AcceptanceStatus,
item.AcceptanceNote,
item.ClipCompilationUrl,
item.ClipCompilationTitle,
item.ClipCompilationPlatform,
item.ClipEmbedStatus))
.ToArray();
}
private static int? ResolveCandidateAvgViewers(AdminCandidateRow candidate, AdminNominationRow[] nominationRows)
{
var directMatch = nominationRows
.Where(item => item.CandidateId == candidate.Id && item.AvgViewers.HasValue)
.OrderByDescending(item => item.TrackerCheckedAt ?? item.ReviewedAt ?? item.CreatedAt)
.Select(item => item.AvgViewers)
.FirstOrDefault();
if (directMatch.HasValue)
{
return directMatch.Value;
}
if (candidate.StreamerIdentityId.HasValue)
{
var identityMatch = nominationRows
.Where(item => item.StreamerIdentityId == candidate.StreamerIdentityId && item.AvgViewers.HasValue)
.OrderByDescending(item => item.TrackerCheckedAt ?? item.ReviewedAt ?? item.CreatedAt)
.Select(item => item.AvgViewers)
.FirstOrDefault();
if (identityMatch.HasValue)
{
return identityMatch.Value;
}
}
var normalizedChannel = candidate.ChannelSlug.Trim().TrimStart('@').ToLowerInvariant();
if (string.IsNullOrWhiteSpace(normalizedChannel))
{
return null;
}
return nominationRows
.Where(item =>
item.AvgViewers.HasValue
&& string.Equals(item.ResolvedChannel?.Trim().TrimStart('@'), normalizedChannel, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(item => item.TrackerCheckedAt ?? item.ReviewedAt ?? item.CreatedAt)
.Select(item => item.AvgViewers)
.FirstOrDefault();
}
private static AdminNominationReviewItemDto ToNominationReviewItem(
AdminNominationRow item,
IEnumerable<dynamic> categoryRows,
@@ -60,6 +60,10 @@ public static partial class AdminSeasonManagementEndpoints
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, Backend.Security.AdminPermissionCatalog.Candidates))
.WithName("UpdateAdminCandidate")
.WithOpenApi();
group.MapGet("/candidates/{candidateId:int}/delete-preview", GetCandidateDeletePreview)
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, Backend.Security.AdminPermissionCatalog.Candidates))
.WithName("GetAdminCandidateDeletePreview")
.WithOpenApi();
group.MapDelete("/candidates/{candidateId:int}", DeleteCandidate)
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, Backend.Security.AdminPermissionCatalog.Candidates))
.WithName("DeleteAdminCandidate")
+239 -78
View File
@@ -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,9 +81,175 @@ public static class AdminSiteSettingsEndpoints
return Results.NotFound();
}
return Results.Ok(new AdminSiteSettingsResponse(
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(
HttpContext context,
UpdateSiteSettingsRequest? request,
AwardsDbContext db,
IAdminAuditService adminAuditService)
{
var requestValidationError = ValidateSiteSettingsRequest(request);
if (requestValidationError is not null)
{
return requestValidationError;
}
var validatedRequest = request!;
var session = AdminEndpointConventions.CurrentSession(context);
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
if (settings is null)
{
return Results.NotFound();
}
var urlValidationError = NormalizeSiteSettingsUrls(validatedRequest, out var normalizedUrls, out var socialLinks);
if (urlValidationError is not null)
{
return urlValidationError;
}
settings.HostDisplayName = validatedRequest.HostDisplayName.Trim();
settings.HostTagline = validatedRequest.HostTagline.Trim();
settings.HostArtistName = validatedRequest.HostArtistName.Trim();
settings.NewsletterUrl = normalizedUrls.NewsletterUrl;
settings.ShareXUrl = normalizedUrls.ShareXUrl;
settings.ShareDiscordUrl = normalizedUrls.ShareDiscordUrl;
settings.PrivacyEmail = validatedRequest.PrivacyEmail.Trim();
var trimmedPrivacyContent = validatedRequest.PrivacyPolicyContent.Trim();
var privacyChanged = !string.Equals(settings.PrivacyPolicyContent, trimmedPrivacyContent, StringComparison.Ordinal);
settings.PrivacyPolicyContent = trimmedPrivacyContent;
if (privacyChanged)
{
settings.PrivacyPolicyUpdatedBy = session.DisplayName.Trim();
settings.PrivacyPolicyUpdatedAt = DateTimeOffset.UtcNow;
}
settings.ImprintUrl = normalizedUrls.ImprintUrl;
settings.ImprintContent = validatedRequest.ImprintContent.Trim();
settings.ContactUrl = normalizedUrls.ContactUrl;
settings.ContactContent = validatedRequest.ContactContent.Trim();
settings.SponsorsUrl = normalizedUrls.SponsorsUrl;
settings.SponsorsContent = validatedRequest.SponsorsContent.Trim();
settings.ShowactsUrl = normalizedUrls.ShowactsUrl;
settings.ShowactsContent = validatedRequest.ShowactsContent.Trim();
settings.StreamBannerEyebrow = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerEyebrow);
settings.StreamBannerTitle = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerTitle);
settings.StreamBannerText = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerText);
settings.StreamBannerLiveButtonLabel = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerLiveButtonLabel);
settings.StreamBannerLiveButtonUrl = normalizedUrls.StreamBannerLiveButtonUrl;
settings.StreamBannerLockedButtonLabel = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerLockedButtonLabel);
settings.StreamBannerUseCompletedContent = validatedRequest.StreamBannerUseCompletedContent;
settings.StreamBannerCompletedEyebrow = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerCompletedEyebrow);
settings.StreamBannerCompletedTitle = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerCompletedTitle);
settings.StreamBannerCompletedText = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerCompletedText);
settings.StreamBannerCompletedButtonLabel = SeasonMappings.NormalizePlainTextContent(validatedRequest.StreamBannerCompletedButtonLabel);
settings.StreamBannerCompletedButtonUrl = normalizedUrls.StreamBannerCompletedButtonUrl;
settings.AwardsSectionTitle = SeasonMappings.NormalizePlainTextContent(validatedRequest.AwardsSectionTitle);
settings.AwardsSectionDescription = SeasonMappings.NormalizePlainTextContent(validatedRequest.AwardsSectionDescription);
settings.SubcategoriesSectionTitle = SeasonMappings.NormalizePlainTextContent(validatedRequest.SubcategoriesSectionTitle);
settings.SubcategoriesSectionDescription = SeasonMappings.NormalizePlainTextContent(validatedRequest.SubcategoriesSectionDescription);
settings.SocialLinksJson = JsonSerializer.Serialize(socialLinks);
settings.FaqJson = JsonSerializer.Serialize(validatedRequest.Faq);
settings.ShowactFormSchemaJson = validatedRequest.ShowactFormSchemaJson ?? "[]";
adminAuditService.AddEntry(
session.TwitchUserId,
"site-settings.update",
"site-settings",
settings.Id.ToString(),
"Public Site Settings wurden aktualisiert.",
new
{
settings.HostDisplayName,
privacyChanged,
socialLinkCount = socialLinks.Length,
faqCount = validatedRequest.Faq.Length,
},
RequestMetadataReader.Read(context));
await db.SaveChangesAsync(context.RequestAborted);
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,
@@ -104,89 +283,19 @@ public static class AdminSiteSettingsEndpoints
SeasonMappings.NormalizePlainTextContent(settings.SubcategoriesSectionDescription),
SeasonMappings.ReadSocialLinks(settings),
SeasonMappings.ReadFaqItems(settings),
settings.ShowactFormSchemaJson ?? "[]"));
settings.ShowactFormSchemaJson ?? "[]");
}
private static async Task<IResult> UpdateSiteSettings(
HttpContext context,
UpdateSiteSettingsRequest request,
AwardsDbContext db,
IAdminAuditService adminAuditService)
internal static string BuildHostImageUrl(SiteSettings settings)
{
var session = AdminEndpointConventions.CurrentSession(context);
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
if (settings is null)
if (settings.HostImageData is not { Length: > 0 })
{
return Results.NotFound();
return DefaultHostImageUrl;
}
var urlValidationError = NormalizeSiteSettingsUrls(request, out var normalizedUrls, out var socialLinks);
if (urlValidationError is not null)
{
return urlValidationError;
}
settings.HostDisplayName = request.HostDisplayName.Trim();
settings.HostTagline = request.HostTagline.Trim();
settings.NewsletterUrl = normalizedUrls.NewsletterUrl;
settings.ShareXUrl = normalizedUrls.ShareXUrl;
settings.ShareDiscordUrl = normalizedUrls.ShareDiscordUrl;
settings.PrivacyEmail = request.PrivacyEmail.Trim();
var trimmedPrivacyContent = request.PrivacyPolicyContent.Trim();
var privacyChanged = !string.Equals(settings.PrivacyPolicyContent, trimmedPrivacyContent, StringComparison.Ordinal);
settings.PrivacyPolicyContent = trimmedPrivacyContent;
if (privacyChanged)
{
settings.PrivacyPolicyUpdatedBy = session.DisplayName.Trim();
settings.PrivacyPolicyUpdatedAt = DateTimeOffset.UtcNow;
}
settings.ImprintUrl = normalizedUrls.ImprintUrl;
settings.ImprintContent = request.ImprintContent.Trim();
settings.ContactUrl = normalizedUrls.ContactUrl;
settings.ContactContent = request.ContactContent.Trim();
settings.SponsorsUrl = normalizedUrls.SponsorsUrl;
settings.SponsorsContent = request.SponsorsContent.Trim();
settings.ShowactsUrl = normalizedUrls.ShowactsUrl;
settings.ShowactsContent = request.ShowactsContent.Trim();
settings.StreamBannerEyebrow = SeasonMappings.NormalizePlainTextContent(request.StreamBannerEyebrow);
settings.StreamBannerTitle = SeasonMappings.NormalizePlainTextContent(request.StreamBannerTitle);
settings.StreamBannerText = SeasonMappings.NormalizePlainTextContent(request.StreamBannerText);
settings.StreamBannerLiveButtonLabel = SeasonMappings.NormalizePlainTextContent(request.StreamBannerLiveButtonLabel);
settings.StreamBannerLiveButtonUrl = normalizedUrls.StreamBannerLiveButtonUrl;
settings.StreamBannerLockedButtonLabel = SeasonMappings.NormalizePlainTextContent(request.StreamBannerLockedButtonLabel);
settings.StreamBannerUseCompletedContent = request.StreamBannerUseCompletedContent;
settings.StreamBannerCompletedEyebrow = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedEyebrow);
settings.StreamBannerCompletedTitle = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedTitle);
settings.StreamBannerCompletedText = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedText);
settings.StreamBannerCompletedButtonLabel = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedButtonLabel);
settings.StreamBannerCompletedButtonUrl = normalizedUrls.StreamBannerCompletedButtonUrl;
settings.AwardsSectionTitle = SeasonMappings.NormalizePlainTextContent(request.AwardsSectionTitle);
settings.AwardsSectionDescription = SeasonMappings.NormalizePlainTextContent(request.AwardsSectionDescription);
settings.SubcategoriesSectionTitle = SeasonMappings.NormalizePlainTextContent(request.SubcategoriesSectionTitle);
settings.SubcategoriesSectionDescription = SeasonMappings.NormalizePlainTextContent(request.SubcategoriesSectionDescription);
settings.SocialLinksJson = JsonSerializer.Serialize(socialLinks);
settings.FaqJson = JsonSerializer.Serialize(request.Faq ?? []);
settings.ShowactFormSchemaJson = request.ShowactFormSchemaJson ?? "[]";
adminAuditService.AddEntry(
session.TwitchUserId,
"site-settings.update",
"site-settings",
settings.Id.ToString(),
"Public Site Settings wurden aktualisiert.",
new
{
settings.HostDisplayName,
privacyChanged,
socialLinkCount = socialLinks.Length,
faqCount = request.Faq?.Length ?? 0,
},
RequestMetadataReader.Read(context));
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new { saved = true });
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(
@@ -238,6 +347,58 @@ public static class AdminSiteSettingsEndpoints
return null;
}
private static IResult? ValidateSiteSettingsRequest(UpdateSiteSettingsRequest? request)
{
if (request is null)
{
return Results.BadRequest(new { message = "Public Site Settings brauchen einen Request-Body." });
}
if (request.SocialLinks is null || request.Faq is null)
{
return Results.BadRequest(new { message = "Public Site Settings brauchen Social Links und FAQ im Request-Body." });
}
string?[] requiredTextFields =
[
request.HostDisplayName,
request.HostTagline,
request.HostArtistName,
request.NewsletterUrl,
request.ShareXUrl,
request.ShareDiscordUrl,
request.PrivacyEmail,
request.PrivacyPolicyContent,
request.ImprintUrl,
request.ImprintContent,
request.ContactUrl,
request.ContactContent,
request.SponsorsUrl,
request.SponsorsContent,
request.ShowactsUrl,
request.ShowactsContent,
request.StreamBannerEyebrow,
request.StreamBannerTitle,
request.StreamBannerText,
request.StreamBannerLiveButtonLabel,
request.StreamBannerLiveButtonUrl,
request.StreamBannerLockedButtonLabel,
request.StreamBannerCompletedEyebrow,
request.StreamBannerCompletedTitle,
request.StreamBannerCompletedText,
request.StreamBannerCompletedButtonLabel,
request.StreamBannerCompletedButtonUrl,
request.AwardsSectionTitle,
request.AwardsSectionDescription,
request.SubcategoriesSectionTitle,
request.SubcategoriesSectionDescription,
];
return requiredTextFields.Any(field => field is null)
? Results.BadRequest(new { message = "Public Site Settings brauchen alle Pflichtfelder im Request-Body." })
: null;
}
private static bool TryNormalizePublicUrl(string? value, string fieldName, out string normalizedUrl, out string errorMessage)
{
normalizedUrl = (value ?? string.Empty).Trim();
+10 -5
View File
@@ -15,16 +15,21 @@ public static partial class AuthEndpoints
private static async Task<IResult> TeamLogin(
HttpContext context,
TeamLoginRequest request,
TeamLoginRequest? request,
AwardsDbContext db,
IUserSessionService userSessionService)
{
var login = NormalizeTeamLogin(request.Login);
if (request is null)
{
return Results.BadRequest(new { message = "Login und Passwort sind erforderlich." });
}
var login = NormalizeTeamLogin(request.Login ?? string.Empty);
var password = request.Password ?? string.Empty;
if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password))
{
return Results.Unauthorized();
return Results.BadRequest(new { message = "Login und Passwort sind erforderlich." });
}
var member = await db.TeamMembers.FirstOrDefaultAsync(item => item.Login == login, context.RequestAborted);
@@ -120,8 +125,8 @@ public static partial class AuthEndpoints
: await db.TeamMembers.FirstOrDefaultAsync(item => item.BoundTwitchUserId == twitchUserId, cancellationToken);
}
private static string NormalizeTeamLogin(string value) =>
value.Trim().TrimStart('@').ToLowerInvariant();
private static string NormalizeTeamLogin(string? value) =>
(value ?? string.Empty).Trim().TrimStart('@').ToLowerInvariant();
private static string NormalizeTwitchUserId(string? value) =>
(value ?? string.Empty).Trim().TrimStart('@').ToLowerInvariant();
+17 -10
View File
@@ -11,13 +11,20 @@ public static partial class PublicEndpoints
{
private static async Task<IResult> CreateClip(
HttpContext context,
CreateClipRequest request,
CreateClipRequest? request,
AwardsDbContext db,
IUserSessionService userSessionService,
IRiskFlagService riskFlagService,
IRiskRuleService riskRuleService)
{
if (!TryNormalizeExternalUrl(request.ClipUrl, out var clipUrl))
var validationError = ValidateCreateClipRequest(request);
if (validationError is not null)
{
return validationError;
}
var validatedRequest = request!;
if (!TryNormalizeExternalUrl(validatedRequest.ClipUrl, out var clipUrl))
{
return Results.BadRequest(new { message = "A valid http(s) clip link is required." });
}
@@ -44,7 +51,7 @@ public static partial class PublicEndpoints
return Results.BadRequest(new { message });
}
var season = await db.Seasons.FirstOrDefaultAsync(item => item.Year == request.Year);
var season = await db.Seasons.FirstOrDefaultAsync(item => item.Year == validatedRequest.Year);
var clipSeasonResolution = EnsurePublicWriteSeason(season, "nomination");
if (clipSeasonResolution.Result is not null)
{
@@ -53,26 +60,26 @@ public static partial class PublicEndpoints
season = clipSeasonResolution.Season!;
var selectedCandidate = request.CandidateId is int candidateId
var selectedCandidate = validatedRequest.CandidateId is int candidateId
? await db.Candidates
.AsNoTracking()
.FirstOrDefaultAsync(item => item.Id == candidateId && item.SeasonId == season.Id)
: null;
if (request.CandidateId is not null && selectedCandidate is null)
if (validatedRequest.CandidateId is not null && selectedCandidate is null)
{
return Results.BadRequest(new { message = "The selected candidate does not exist for this season." });
}
if (request.CategoryId is int requestedCategoryId
if (validatedRequest.CategoryId is int requestedCategoryId
&& selectedCandidate is not null
&& selectedCandidate.CategoryId != requestedCategoryId)
{
return Results.BadRequest(new { message = "The selected candidate does not belong to the selected category." });
}
var resolvedCategoryId = request.CategoryId ?? selectedCandidate?.CategoryId;
var normalizedTitle = request.Title?.Trim() ?? string.Empty;
var submittedCreator = request.Creator?.Trim();
var resolvedCategoryId = validatedRequest.CategoryId ?? selectedCandidate?.CategoryId;
var normalizedTitle = validatedRequest.Title?.Trim() ?? string.Empty;
var submittedCreator = validatedRequest.Creator?.Trim();
var normalizedCreator = string.IsNullOrWhiteSpace(submittedCreator)
? selectedCandidate?.DisplayName ?? string.Empty
: submittedCreator;
@@ -96,7 +103,7 @@ public static partial class PublicEndpoints
}
}
var submitterIdResult = await ResolveSubmitterIdAsync(context, request.TwitchUserId, userSessionService);
var submitterIdResult = await ResolveSubmitterIdAsync(context, validatedRequest.TwitchUserId, userSessionService);
if (submitterIdResult.Result is not null)
{
return submitterIdResult.Result;
@@ -1,4 +1,5 @@
using Backend.Common;
using Backend.Contracts;
using Backend.Domain;
using Backend.Services;
using Microsoft.AspNetCore.Http.Extensions;
@@ -205,4 +206,74 @@ public static partial class PublicEndpoints
"show" => "show",
_ => "current",
};
private static IResult? ValidateCreateNominationRequest(CreateNominationRequest? request)
{
if (request is null)
{
return Results.BadRequest(new { message = "A nomination request body is required." });
}
if (request.Year <= 0)
{
return Results.BadRequest(new { message = "A valid season year is required." });
}
if ((request.Nominations?.Length ?? 0) == 0 && (request.Nominees?.Length ?? 0) == 0)
{
return Results.BadRequest(new { message = "A nomination request must include at least one stream link." });
}
return null;
}
private static IResult? ValidateCreateVoteRequest(CreateVoteRequest? request)
{
if (request is null)
{
return Results.BadRequest(new { message = "A vote request body is required." });
}
if (request.SeasonId <= 0)
{
return Results.BadRequest(new { message = "A valid season id is required." });
}
if (request.Entries is null || request.Entries.Length == 0)
{
return Results.BadRequest(new { message = "At least one vote entry is required." });
}
if (request.Entries.Any(item => item.CategoryId <= 0 || item.CandidateId <= 0))
{
return Results.BadRequest(new { message = "Each vote entry requires a valid category and candidate." });
}
return null;
}
private static IResult? ValidateCreateClipRequest(CreateClipRequest? request)
{
if (request is null)
{
return Results.BadRequest(new { message = "A clip request body is required." });
}
if (request.Year <= 0)
{
return Results.BadRequest(new { message = "A valid season year is required." });
}
if (request.CategoryId is <= 0 || request.CandidateId is <= 0)
{
return Results.BadRequest(new { message = "Category and candidate ids must be positive when provided." });
}
if (string.IsNullOrWhiteSpace(request.ClipUrl))
{
return Results.BadRequest(new { message = "A valid http(s) clip link is required." });
}
return null;
}
}
+4
View File
@@ -14,6 +14,10 @@ public static partial class PublicEndpoints
.WithName("GetSiteStatus")
.WithOpenApi();
group.MapGet("/host-image", GetHostImage)
.WithName("GetPublicHostImage")
.WithOpenApi();
group.MapGet("/seasons/{year:int}/categories", GetSeasonCategories)
.WithName("GetSeasonCategories")
.WithOpenApi();
@@ -0,0 +1,35 @@
using Backend.Data;
using Microsoft.EntityFrameworkCore;
namespace Backend.Endpoints;
public static partial class PublicEndpoints
{
private static async Task<IResult> GetHostImage(AwardsDbContext db)
{
var settings = await db.SiteSettings
.AsNoTracking()
.Where(item => item.Id == 1)
.Select(item => new
{
item.HostImageData,
item.HostImageContentType,
item.HostImageUpdatedAt,
})
.FirstOrDefaultAsync();
if (settings?.HostImageData is not { Length: > 0 } imageData
|| string.IsNullOrWhiteSpace(settings.HostImageContentType))
{
return Results.Redirect("/assets/amaterasu2sei_2.png", permanent: false);
}
var entityTag = settings.HostImageUpdatedAt?.ToUnixTimeSeconds().ToString(System.Globalization.CultureInfo.InvariantCulture)
?? imageData.Length.ToString(System.Globalization.CultureInfo.InvariantCulture);
return Results.File(
imageData,
settings.HostImageContentType,
entityTag: new Microsoft.Net.Http.Headers.EntityTagHeaderValue($"\"host-{entityTag}\""),
lastModified: settings.HostImageUpdatedAt);
}
}
+12 -5
View File
@@ -11,14 +11,21 @@ public static partial class PublicEndpoints
{
private static async Task<IResult> CreateNomination(
HttpContext context,
CreateNominationRequest request,
CreateNominationRequest? request,
AwardsDbContext db,
IUserSessionService userSessionService,
IRiskFlagService riskFlagService,
IRiskRuleService riskRuleService,
NominationEnrichmentService nominationEnrichmentService)
{
var submittedNominations = NormalizeSubmittedNominations(request);
var validationError = ValidateCreateNominationRequest(request);
if (validationError is not null)
{
return validationError;
}
var validatedRequest = request!;
var submittedNominations = NormalizeSubmittedNominations(validatedRequest);
if (submittedNominations.Length == 0)
{
@@ -66,14 +73,14 @@ public static partial class PublicEndpoints
return Results.BadRequest(new { message = "Dieser Link kann nicht nominiert werden. Bitte reiche einen direkten Kanal- oder Profil-Link ein." });
}
var season = await db.Seasons.FirstOrDefaultAsync(item => item.Year == request.Year);
var season = await db.Seasons.FirstOrDefaultAsync(item => item.Year == validatedRequest.Year);
if (season is null)
{
return Results.BadRequest(new { message = "The selected season does not exist." });
}
var categoryGroupName = await ResolveCategoryGroupNameAsync(db, season.Id, request, context.RequestAborted);
var categoryGroupName = await ResolveCategoryGroupNameAsync(db, season.Id, validatedRequest, context.RequestAborted);
if (string.IsNullOrWhiteSpace(categoryGroupName))
{
return Results.BadRequest(new { message = "The selected category group does not exist for this season." });
@@ -102,7 +109,7 @@ public static partial class PublicEndpoints
return nominationSeasonResolution.Result;
}
var submitterIdResult = await ResolveSubmitterIdAsync(context, request.TwitchUserId, userSessionService);
var submitterIdResult = await ResolveSubmitterIdAsync(context, validatedRequest.TwitchUserId, userSessionService);
if (submitterIdResult.Result is not null)
{
return submitterIdResult.Result;
@@ -89,8 +89,23 @@ public static partial class PublicEndpoints
.OrderByDescending(item => item.Year)
.ToArrayAsync();
var archivedWinnerYearRows = await db.ArchivedWinners
.AsNoTracking()
.Where(item => latestPublishedWinnerYear == null || item.Year < latestPublishedWinnerYear.Value)
.GroupBy(item => item.Year)
.Select(group => new
{
Year = group.Key,
WinnerCount = group.Count(),
})
.ToArrayAsync();
var archiveYears = archiveYearRows
.Select(item => new ArchiveYearDto(item.Year, item.WinnerCount))
.Concat(archivedWinnerYearRows.Select(item => new ArchiveYearDto(item.Year, item.WinnerCount)))
.GroupBy(item => item.Year)
.Select(group => group.Last())
.OrderByDescending(item => item.Year)
.ToArray();
var phaseKey = SeasonMappings.NormalizePhaseKey(season.CurrentPhase);
@@ -150,6 +165,8 @@ public static partial class PublicEndpoints
new PublicSiteContentDto(
siteSettings.HostDisplayName,
siteSettings.HostTagline,
siteSettings.HostArtistName,
AdminSiteSettingsEndpoints.BuildHostImageUrl(siteSettings),
siteSettings.NewsletterUrl,
siteSettings.ShareXUrl,
siteSettings.ShareDiscordUrl,
+19 -17
View File
@@ -12,37 +12,39 @@ public static partial class PublicEndpoints
{
private static async Task<IResult> CreateVote(
HttpContext context,
CreateVoteRequest request,
CreateVoteRequest? request,
AwardsDbContext db,
IUserSessionService userSessionService,
IRiskFlagService riskFlagService,
IRiskRuleService riskRuleService)
{
if (request.Entries.Length == 0)
var validationError = ValidateCreateVoteRequest(request);
if (validationError is not null)
{
return Results.BadRequest(new { message = "At least one vote entry is required." });
return validationError;
}
var distinctCategoryCount = request.Entries
var validatedRequest = request!;
var distinctCategoryCount = validatedRequest.Entries
.Select(item => item.CategoryId)
.Distinct()
.Count();
if (distinctCategoryCount != request.Entries.Length)
if (distinctCategoryCount != validatedRequest.Entries.Length)
{
return Results.BadRequest(new { message = "Only one vote entry per category is allowed." });
}
var season = await db.Seasons
.AsNoTracking()
.FirstOrDefaultAsync(item => item.Id == request.SeasonId);
.FirstOrDefaultAsync(item => item.Id == validatedRequest.SeasonId);
var voteSeasonResolution = EnsurePublicWriteSeason(season, "voting");
if (voteSeasonResolution.Result is not null)
{
return voteSeasonResolution.Result;
}
var submitterIdResult = await ResolveSubmitterIdAsync(context, request.TwitchUserId, userSessionService);
var submitterIdResult = await ResolveSubmitterIdAsync(context, validatedRequest.TwitchUserId, userSessionService);
if (submitterIdResult.Result is not null)
{
return submitterIdResult.Result;
@@ -50,10 +52,10 @@ public static partial class PublicEndpoints
var submitterId = submitterIdResult.SubmitterId!;
var requestMetadata = RequestMetadataReader.Read(context);
var candidateIds = request.Entries.Select(item => item.CandidateId).Distinct().ToArray();
var candidateIds = validatedRequest.Entries.Select(item => item.CandidateId).Distinct().ToArray();
var validCandidates = await db.Candidates
.AsNoTracking()
.Where(item => item.SeasonId == request.SeasonId && candidateIds.Contains(item.Id))
.Where(item => item.SeasonId == validatedRequest.SeasonId && candidateIds.Contains(item.Id))
.Select(item => new { item.Id, item.CategoryId })
.ToArrayAsync();
@@ -63,17 +65,17 @@ public static partial class PublicEndpoints
}
var candidateCategoryMap = validCandidates.ToDictionary(item => item.Id, item => item.CategoryId);
if (request.Entries.Any(item => candidateCategoryMap[item.CandidateId] != item.CategoryId))
if (validatedRequest.Entries.Any(item => candidateCategoryMap[item.CandidateId] != item.CategoryId))
{
return Results.BadRequest(new { message = "A selected candidate does not match the submitted category." });
}
var ballot = await db.VoteBallots
.Include(item => item.Entries)
.FirstOrDefaultAsync(item => item.SeasonId == request.SeasonId && item.SubmittedByTwitchId == submitterId);
.FirstOrDefaultAsync(item => item.SeasonId == validatedRequest.SeasonId && item.SubmittedByTwitchId == submitterId);
var isResubmission = ballot is not null;
ballot = await ApplyVoteEntriesAsync(db, ballot, request.SeasonId, submitterId, request.Entries, context.RequestAborted);
ballot = await ApplyVoteEntriesAsync(db, ballot, validatedRequest.SeasonId, submitterId, validatedRequest.Entries, context.RequestAborted);
var resubmittedBallotRule = await riskRuleService.GetRuleAsync("resubmitted_ballot", context.RequestAborted);
var rapidVoteUpdatesRule = await riskRuleService.GetRuleAsync("rapid_vote_updates", context.RequestAborted);
@@ -90,9 +92,9 @@ public static partial class PublicEndpoints
db.ChangeTracker.Clear();
ballot = await db.VoteBallots
.Include(item => item.Entries)
.FirstAsync(item => item.SeasonId == request.SeasonId && item.SubmittedByTwitchId == submitterId, context.RequestAborted);
.FirstAsync(item => item.SeasonId == validatedRequest.SeasonId && item.SubmittedByTwitchId == submitterId, context.RequestAborted);
isResubmission = true;
ballot = await ApplyVoteEntriesAsync(db, ballot, request.SeasonId, submitterId, request.Entries, context.RequestAborted);
ballot = await ApplyVoteEntriesAsync(db, ballot, validatedRequest.SeasonId, submitterId, validatedRequest.Entries, context.RequestAborted);
await db.SaveChangesAsync(context.RequestAborted);
}
@@ -107,21 +109,21 @@ public static partial class PublicEndpoints
if (isResubmission && resubmittedBallotRule.Enabled)
{
await riskFlagService.AddIfMissingAsync(
request.SeasonId,
validatedRequest.SeasonId,
submitterId,
"vote",
"resubmitted_ballot",
resubmittedBallotRule.Severity,
"Ein User hat sein Ballot erneut gespeichert oder aktualisiert.",
requestMetadata,
new { ballotId = ballot.Id, entryCount = request.Entries.Length, entityLinks = new[] { ballotLink } },
new { ballotId = ballot.Id, entryCount = validatedRequest.Entries.Length, entityLinks = new[] { ballotLink } },
context.RequestAborted);
}
if (rapidVoteUpdatesRule.Enabled && recentVoteSubmissions >= rapidVoteUpdatesRule.Threshold)
{
await riskFlagService.AddIfMissingAsync(
request.SeasonId,
validatedRequest.SeasonId,
submitterId,
"vote",
"rapid_vote_updates",
@@ -9,6 +9,42 @@ public static partial class PublicEndpoints
{
private static async Task<IResult> GetWinnerArchive(int year, AwardsDbContext db)
{
var latestPublishedWinnerYear = await db.Results
.AsNoTracking()
.Where(result => result.Season.WinnersPublishedAt != null)
.Select(result => (int?)result.Season.Year)
.MaxAsync();
var archivedWinnerRows = await db.ArchivedWinners
.AsNoTracking()
.Where(item => item.Year == year)
.OrderBy(item => item.Category)
.ThenBy(item => item.Subcategory)
.ThenBy(item => item.WinnerName)
.ToArrayAsync();
if (archivedWinnerRows.Length > 0)
{
var archivedItems = archivedWinnerRows
.Select(item =>
{
var metadata = SeasonMappings.InferProfileMetadataFromUrl(item.WinnerUrl, item.WinnerName);
return new WinnerArchiveItemDto(
item.Subcategory,
item.Category,
item.WinnerName,
metadata.Slug,
metadata.Platform,
item.WinnerUrl,
null,
null,
null,
null);
})
.ToArray();
return Results.Ok(new WinnerArchiveResponse(year, archivedItems));
}
var season = await db.Seasons
.AsNoTracking()
.Where(item => item.Year == year)
@@ -16,7 +52,7 @@ public static partial class PublicEndpoints
.FirstOrDefaultAsync();
if (season is null)
{
return Results.NotFound();
return Results.Ok(new WinnerArchiveResponse(year, []));
}
if (season.WinnersPublishedAt is null)
@@ -24,11 +60,6 @@ public static partial class PublicEndpoints
return Results.Ok(new WinnerArchiveResponse(year, []));
}
var latestPublishedWinnerYear = await db.Results
.AsNoTracking()
.Where(result => result.Season.WinnersPublishedAt != null)
.Select(result => (int?)result.Season.Year)
.MaxAsync();
if (latestPublishedWinnerYear == season.Year)
{
return Results.Ok(new WinnerArchiveResponse(year, []));
@@ -0,0 +1,833 @@
// <auto-generated />
using System;
using Backend.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Backend.Migrations
{
[DbContext(typeof(AwardsDbContext))]
[Migration("20260617060000_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Backend.Domain.AwardResult", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("CandidateId")
.HasColumnType("integer");
b.Property<string>("CategoryName")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<int>("SeasonId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("CandidateId");
b.HasIndex("SeasonId");
b.ToTable("Results");
b.HasData(
new
{
Id = 1,
CandidateId = 8,
CategoryName = "VTuber des Jahres",
SeasonId = 2
},
new
{
Id = 2,
CandidateId = 9,
CategoryName = "Bestes Live Event",
SeasonId = 2
},
new
{
Id = 3,
CandidateId = 10,
CategoryName = "Clip des Jahres",
SeasonId = 2
},
new
{
Id = 4,
CandidateId = 11,
CategoryName = "VTuber des Jahres",
SeasonId = 3
},
new
{
Id = 5,
CandidateId = 12,
CategoryName = "Clip des Jahres",
SeasonId = 3
},
new
{
Id = 6,
CandidateId = 13,
CategoryName = "VTuber des Jahres",
SeasonId = 4
});
});
modelBuilder.Entity("Backend.Domain.Candidate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("CategoryId")
.HasColumnType("integer");
b.Property<string>("ChannelSlug")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("Platform")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<int>("SeasonId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("SeasonId");
b.ToTable("Candidates");
b.HasData(
new
{
Id = 1,
CategoryId = 1,
ChannelSlug = "@hoshimimiyu",
DisplayName = "Hoshimi Miyu",
Platform = "Twitch",
SeasonId = 1
},
new
{
Id = 2,
CategoryId = 1,
ChannelSlug = "@kurainu",
DisplayName = "Kurainu",
Platform = "Twitch",
SeasonId = 1
},
new
{
Id = 3,
CategoryId = 1,
ChannelSlug = "@shiroch",
DisplayName = "Shiro Ch.",
Platform = "Twitch",
SeasonId = 1
},
new
{
Id = 4,
CategoryId = 2,
ChannelSlug = "@kurainu",
DisplayName = "Kurainu 3D Live",
Platform = "Twitch",
SeasonId = 1
},
new
{
Id = 5,
CategoryId = 2,
ChannelSlug = "@aoisakura",
DisplayName = "Aoi Sakura Showcase",
Platform = "YouTube",
SeasonId = 1
},
new
{
Id = 6,
CategoryId = 3,
ChannelSlug = "@pyonkichikingdom",
DisplayName = "Pyonkichi Kingdom",
Platform = "Twitch",
SeasonId = 1
},
new
{
Id = 7,
CategoryId = 4,
ChannelSlug = "@moonrelay",
DisplayName = "Moonrelay",
Platform = "Twitch",
SeasonId = 1
},
new
{
Id = 8,
CategoryId = 5,
ChannelSlug = "@hoshimimiyu",
DisplayName = "Hoshimi Miyu",
Platform = "Twitch",
SeasonId = 2
},
new
{
Id = 9,
CategoryId = 6,
ChannelSlug = "@kurainu",
DisplayName = "Kurainu 3D Live",
Platform = "Twitch",
SeasonId = 2
},
new
{
Id = 10,
CategoryId = 7,
ChannelSlug = "@pyonkichikingdom",
DisplayName = "Pyonkichi Kingdom",
Platform = "Twitch",
SeasonId = 2
},
new
{
Id = 11,
CategoryId = 8,
ChannelSlug = "@aoisakura",
DisplayName = "Aoi Sakura",
Platform = "YouTube",
SeasonId = 3
},
new
{
Id = 12,
CategoryId = 9,
ChannelSlug = "@starbyte",
DisplayName = "Starbyte",
Platform = "Twitch",
SeasonId = 3
},
new
{
Id = 13,
CategoryId = 10,
ChannelSlug = "@tenshivox",
DisplayName = "Tenshi Vox",
Platform = "Twitch",
SeasonId = 4
});
});
modelBuilder.Entity("Backend.Domain.Category", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<string>("GroupName")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<int>("MaxNomineesPerUser")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<int>("SeasonId")
.HasColumnType("integer");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("text");
b.Property<int>("SortOrder")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("SeasonId", "Slug")
.IsUnique();
b.ToTable("Categories");
b.HasData(
new
{
Id = 1,
Description = "Die groesste Auszeichnung des Jahres.",
GroupName = "Main Awards",
MaxNomineesPerUser = 3,
Name = "VTuber des Jahres",
SeasonId = 1,
Slug = "vtuber-des-jahres",
SortOrder = 1
},
new
{
Id = 2,
Description = "Events, Konzerte und 3D-Shows.",
GroupName = "Performance",
MaxNomineesPerUser = 3,
Name = "Bestes Live Event",
SeasonId = 1,
Slug = "bestes-live-event",
SortOrder = 2
},
new
{
Id = 3,
Description = "Der lustigste oder emotionalste Clip des Jahres.",
GroupName = "Clips & Highlights",
MaxNomineesPerUser = 3,
Name = "Clip des Jahres",
SeasonId = 1,
Slug = "clip-des-jahres",
SortOrder = 3
},
new
{
Id = 4,
Description = "Die aktivste und freundlichste Community.",
GroupName = "Main Awards",
MaxNomineesPerUser = 3,
Name = "Beste Community",
SeasonId = 1,
Slug = "beste-community",
SortOrder = 4
},
new
{
Id = 5,
Description = "Archivkategorie 2025.",
GroupName = "Main Awards",
MaxNomineesPerUser = 3,
Name = "VTuber des Jahres",
SeasonId = 2,
Slug = "vtuber-des-jahres",
SortOrder = 1
},
new
{
Id = 6,
Description = "Archivkategorie 2025.",
GroupName = "Performance",
MaxNomineesPerUser = 3,
Name = "Bestes Live Event",
SeasonId = 2,
Slug = "bestes-live-event",
SortOrder = 2
},
new
{
Id = 7,
Description = "Archivkategorie 2025.",
GroupName = "Clips & Highlights",
MaxNomineesPerUser = 3,
Name = "Clip des Jahres",
SeasonId = 2,
Slug = "clip-des-jahres",
SortOrder = 3
},
new
{
Id = 8,
Description = "Archivkategorie 2024.",
GroupName = "Main Awards",
MaxNomineesPerUser = 3,
Name = "VTuber des Jahres",
SeasonId = 3,
Slug = "vtuber-des-jahres",
SortOrder = 1
},
new
{
Id = 9,
Description = "Archivkategorie 2024.",
GroupName = "Clips & Highlights",
MaxNomineesPerUser = 3,
Name = "Clip des Jahres",
SeasonId = 3,
Slug = "clip-des-jahres",
SortOrder = 2
},
new
{
Id = 10,
Description = "Archivkategorie 2023.",
GroupName = "Main Awards",
MaxNomineesPerUser = 3,
Name = "VTuber des Jahres",
SeasonId = 4,
Slug = "vtuber-des-jahres",
SortOrder = 1
});
});
modelBuilder.Entity("Backend.Domain.Nomination", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("CandidateId")
.HasColumnType("integer");
b.Property<string>("CandidateText")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<int>("CategoryId")
.HasColumnType("integer");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("SeasonId")
.HasColumnType("integer");
b.Property<string>("SubmittedByTwitchId")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.HasKey("Id");
b.HasIndex("CandidateId");
b.HasIndex("CategoryId");
b.HasIndex("SeasonId");
b.ToTable("Nominations");
b.HasData(
new
{
Id = 1,
CandidateText = "Hoshimi Miyu",
CategoryId = 1,
CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
SeasonId = 1,
SubmittedByTwitchId = "twitch_hoshi"
},
new
{
Id = 2,
CandidateText = "Kurainu 3D Live",
CategoryId = 2,
CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
SeasonId = 1,
SubmittedByTwitchId = "twitch_kurainu"
});
});
modelBuilder.Entity("Backend.Domain.Season", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("CurrentPhase")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<bool>("IsCommunityOnly")
.HasColumnType("boolean");
b.Property<bool>("IsCurrent")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<DateOnly>("NominationEndsAt")
.HasColumnType("date");
b.Property<DateOnly>("NominationStartsAt")
.HasColumnType("date");
b.Property<DateOnly>("ReviewEndsAt")
.HasColumnType("date");
b.Property<DateOnly>("ReviewStartsAt")
.HasColumnType("date");
b.Property<DateOnly>("ShowDate")
.HasColumnType("date");
b.Property<DateOnly>("VotingEndsAt")
.HasColumnType("date");
b.Property<DateOnly>("VotingStartsAt")
.HasColumnType("date");
b.Property<int>("Year")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Year")
.IsUnique();
b.ToTable("Seasons");
b.HasData(
new
{
Id = 1,
CurrentPhase = "Community Voting",
IsCommunityOnly = true,
IsCurrent = true,
Name = "VTuber Star Awards 2026",
NominationEndsAt = new DateOnly(2026, 5, 31),
NominationStartsAt = new DateOnly(2026, 5, 1),
ReviewEndsAt = new DateOnly(2026, 7, 10),
ReviewStartsAt = new DateOnly(2026, 7, 1),
ShowDate = new DateOnly(2026, 7, 20),
VotingEndsAt = new DateOnly(2026, 6, 30),
VotingStartsAt = new DateOnly(2026, 6, 1),
Year = 2026
},
new
{
Id = 2,
CurrentPhase = "Archived",
IsCommunityOnly = true,
IsCurrent = false,
Name = "VTuber Star Awards 2025",
NominationEndsAt = new DateOnly(2025, 5, 31),
NominationStartsAt = new DateOnly(2025, 5, 1),
ReviewEndsAt = new DateOnly(2025, 7, 10),
ReviewStartsAt = new DateOnly(2025, 7, 1),
ShowDate = new DateOnly(2025, 7, 20),
VotingEndsAt = new DateOnly(2025, 6, 30),
VotingStartsAt = new DateOnly(2025, 6, 1),
Year = 2025
},
new
{
Id = 3,
CurrentPhase = "Archived",
IsCommunityOnly = true,
IsCurrent = false,
Name = "VTuber Star Awards 2024",
NominationEndsAt = new DateOnly(2024, 5, 31),
NominationStartsAt = new DateOnly(2024, 5, 1),
ReviewEndsAt = new DateOnly(2024, 7, 10),
ReviewStartsAt = new DateOnly(2024, 7, 1),
ShowDate = new DateOnly(2024, 7, 20),
VotingEndsAt = new DateOnly(2024, 6, 30),
VotingStartsAt = new DateOnly(2024, 6, 1),
Year = 2024
},
new
{
Id = 4,
CurrentPhase = "Archived",
IsCommunityOnly = true,
IsCurrent = false,
Name = "VTuber Star Awards 2023",
NominationEndsAt = new DateOnly(2023, 5, 31),
NominationStartsAt = new DateOnly(2023, 5, 1),
ReviewEndsAt = new DateOnly(2023, 7, 10),
ReviewStartsAt = new DateOnly(2023, 7, 1),
ShowDate = new DateOnly(2023, 7, 20),
VotingEndsAt = new DateOnly(2023, 6, 30),
VotingStartsAt = new DateOnly(2023, 6, 1),
Year = 2023
});
});
modelBuilder.Entity("Backend.Domain.VoteBallot", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("SeasonId")
.HasColumnType("integer");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<DateTimeOffset>("SubmittedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("SubmittedByTwitchId")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.HasKey("Id");
b.HasIndex("SeasonId");
b.ToTable("VoteBallots");
b.HasData(
new
{
Id = 1,
SeasonId = 1,
Status = "submitted",
SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
SubmittedByTwitchId = "twitch_vote_1"
},
new
{
Id = 2,
SeasonId = 1,
Status = "submitted",
SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
SubmittedByTwitchId = "twitch_vote_2"
});
});
modelBuilder.Entity("Backend.Domain.VoteEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("BallotId")
.HasColumnType("integer");
b.Property<int>("CandidateId")
.HasColumnType("integer");
b.Property<int>("CategoryId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("BallotId");
b.HasIndex("CandidateId");
b.HasIndex("CategoryId");
b.ToTable("VoteEntries");
b.HasData(
new
{
Id = 1,
BallotId = 1,
CandidateId = 1,
CategoryId = 1
},
new
{
Id = 2,
BallotId = 1,
CandidateId = 4,
CategoryId = 2
},
new
{
Id = 3,
BallotId = 2,
CandidateId = 2,
CategoryId = 1
},
new
{
Id = 4,
BallotId = 2,
CandidateId = 6,
CategoryId = 3
});
});
modelBuilder.Entity("Backend.Domain.AwardResult", b =>
{
b.HasOne("Backend.Domain.Candidate", "Candidate")
.WithMany()
.HasForeignKey("CandidateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Backend.Domain.Season", "Season")
.WithMany("Results")
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Candidate");
b.Navigation("Season");
});
modelBuilder.Entity("Backend.Domain.Candidate", b =>
{
b.HasOne("Backend.Domain.Category", "Category")
.WithMany("Candidates")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Backend.Domain.Season", "Season")
.WithMany()
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
b.Navigation("Season");
});
modelBuilder.Entity("Backend.Domain.Category", b =>
{
b.HasOne("Backend.Domain.Season", "Season")
.WithMany("Categories")
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Season");
});
modelBuilder.Entity("Backend.Domain.Nomination", b =>
{
b.HasOne("Backend.Domain.Candidate", "Candidate")
.WithMany()
.HasForeignKey("CandidateId");
b.HasOne("Backend.Domain.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Backend.Domain.Season", "Season")
.WithMany()
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Candidate");
b.Navigation("Category");
b.Navigation("Season");
});
modelBuilder.Entity("Backend.Domain.VoteBallot", b =>
{
b.HasOne("Backend.Domain.Season", "Season")
.WithMany()
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Season");
});
modelBuilder.Entity("Backend.Domain.VoteEntry", b =>
{
b.HasOne("Backend.Domain.VoteBallot", "Ballot")
.WithMany("Entries")
.HasForeignKey("BallotId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Backend.Domain.Candidate", "Candidate")
.WithMany()
.HasForeignKey("CandidateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Backend.Domain.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ballot");
b.Navigation("Candidate");
b.Navigation("Category");
});
modelBuilder.Entity("Backend.Domain.Category", b =>
{
b.Navigation("Candidates");
});
modelBuilder.Entity("Backend.Domain.Season", b =>
{
b.Navigation("Categories");
b.Navigation("Results");
});
modelBuilder.Entity("Backend.Domain.VoteBallot", b =>
{
b.Navigation("Entries");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,395 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Seasons",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Year = table.Column<int>(type: "integer", nullable: false),
Name = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
IsCurrent = table.Column<bool>(type: "boolean", nullable: false),
IsCommunityOnly = table.Column<bool>(type: "boolean", nullable: false),
CurrentPhase = table.Column<string>(type: "character varying(60)", maxLength: 60, nullable: false),
NominationStartsAt = table.Column<DateOnly>(type: "date", nullable: false),
NominationEndsAt = table.Column<DateOnly>(type: "date", nullable: false),
VotingStartsAt = table.Column<DateOnly>(type: "date", nullable: false),
VotingEndsAt = table.Column<DateOnly>(type: "date", nullable: false),
ReviewStartsAt = table.Column<DateOnly>(type: "date", nullable: false),
ReviewEndsAt = table.Column<DateOnly>(type: "date", nullable: false),
ShowDate = table.Column<DateOnly>(type: "date", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Seasons", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
GroupName = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
Name = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Slug = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
SortOrder = table.Column<int>(type: "integer", nullable: false),
MaxNomineesPerUser = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.Id);
table.ForeignKey(
name: "FK_Categories_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "VoteBallots",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
SubmittedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Status = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
SubmittedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_VoteBallots", x => x.Id);
table.ForeignKey(
name: "FK_VoteBallots_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Candidates",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
CategoryId = table.Column<int>(type: "integer", nullable: false),
DisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
ChannelSlug = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Platform = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Candidates", x => x.Id);
table.ForeignKey(
name: "FK_Candidates_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Candidates_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Nominations",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
CategoryId = table.Column<int>(type: "integer", nullable: false),
SubmittedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
CandidateId = table.Column<int>(type: "integer", nullable: true),
CandidateText = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Nominations", x => x.Id);
table.ForeignKey(
name: "FK_Nominations_Candidates_CandidateId",
column: x => x.CandidateId,
principalTable: "Candidates",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Nominations_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Nominations_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Results",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
CandidateId = table.Column<int>(type: "integer", nullable: false),
CategoryName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Results", x => x.Id);
table.ForeignKey(
name: "FK_Results_Candidates_CandidateId",
column: x => x.CandidateId,
principalTable: "Candidates",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Results_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "VoteEntries",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
BallotId = table.Column<int>(type: "integer", nullable: false),
CategoryId = table.Column<int>(type: "integer", nullable: false),
CandidateId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_VoteEntries", x => x.Id);
table.ForeignKey(
name: "FK_VoteEntries_Candidates_CandidateId",
column: x => x.CandidateId,
principalTable: "Candidates",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_VoteEntries_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_VoteEntries_VoteBallots_BallotId",
column: x => x.BallotId,
principalTable: "VoteBallots",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
table: "Seasons",
columns: new[] { "Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year" },
values: new object[,]
{
{ 1, "Community Voting", true, true, "VTuber Star Awards 2026", new DateOnly(2026, 5, 31), new DateOnly(2026, 5, 1), new DateOnly(2026, 7, 10), new DateOnly(2026, 7, 1), new DateOnly(2026, 7, 20), new DateOnly(2026, 6, 30), new DateOnly(2026, 6, 1), 2026 },
{ 2, "Archived", true, false, "VTuber Star Awards 2025", new DateOnly(2025, 5, 31), new DateOnly(2025, 5, 1), new DateOnly(2025, 7, 10), new DateOnly(2025, 7, 1), new DateOnly(2025, 7, 20), new DateOnly(2025, 6, 30), new DateOnly(2025, 6, 1), 2025 },
{ 3, "Archived", true, false, "VTuber Star Awards 2024", new DateOnly(2024, 5, 31), new DateOnly(2024, 5, 1), new DateOnly(2024, 7, 10), new DateOnly(2024, 7, 1), new DateOnly(2024, 7, 20), new DateOnly(2024, 6, 30), new DateOnly(2024, 6, 1), 2024 },
{ 4, "Archived", true, false, "VTuber Star Awards 2023", new DateOnly(2023, 5, 31), new DateOnly(2023, 5, 1), new DateOnly(2023, 7, 10), new DateOnly(2023, 7, 1), new DateOnly(2023, 7, 20), new DateOnly(2023, 6, 30), new DateOnly(2023, 6, 1), 2023 }
});
migrationBuilder.InsertData(
table: "Categories",
columns: new[] { "Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder" },
values: new object[,]
{
{ 1, "Die groesste Auszeichnung des Jahres.", "Main Awards", 3, "VTuber des Jahres", 1, "vtuber-des-jahres", 1 },
{ 2, "Events, Konzerte und 3D-Shows.", "Performance", 3, "Bestes Live Event", 1, "bestes-live-event", 2 },
{ 3, "Der lustigste oder emotionalste Clip des Jahres.", "Clips & Highlights", 3, "Clip des Jahres", 1, "clip-des-jahres", 3 },
{ 4, "Die aktivste und freundlichste Community.", "Main Awards", 3, "Beste Community", 1, "beste-community", 4 },
{ 5, "Archivkategorie 2025.", "Main Awards", 3, "VTuber des Jahres", 2, "vtuber-des-jahres", 1 },
{ 6, "Archivkategorie 2025.", "Performance", 3, "Bestes Live Event", 2, "bestes-live-event", 2 },
{ 7, "Archivkategorie 2025.", "Clips & Highlights", 3, "Clip des Jahres", 2, "clip-des-jahres", 3 },
{ 8, "Archivkategorie 2024.", "Main Awards", 3, "VTuber des Jahres", 3, "vtuber-des-jahres", 1 },
{ 9, "Archivkategorie 2024.", "Clips & Highlights", 3, "Clip des Jahres", 3, "clip-des-jahres", 2 },
{ 10, "Archivkategorie 2023.", "Main Awards", 3, "VTuber des Jahres", 4, "vtuber-des-jahres", 1 }
});
migrationBuilder.InsertData(
table: "VoteBallots",
columns: new[] { "Id", "SeasonId", "Status", "SubmittedAt", "SubmittedByTwitchId" },
values: new object[,]
{
{ 1, 1, "submitted", new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "twitch_vote_1" },
{ 2, 1, "submitted", new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "twitch_vote_2" }
});
migrationBuilder.InsertData(
table: "Candidates",
columns: new[] { "Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId" },
values: new object[,]
{
{ 1, 1, "@hoshimimiyu", "Hoshimi Miyu", "Twitch", 1 },
{ 2, 1, "@kurainu", "Kurainu", "Twitch", 1 },
{ 3, 1, "@shiroch", "Shiro Ch.", "Twitch", 1 },
{ 4, 2, "@kurainu", "Kurainu 3D Live", "Twitch", 1 },
{ 5, 2, "@aoisakura", "Aoi Sakura Showcase", "YouTube", 1 },
{ 6, 3, "@pyonkichikingdom", "Pyonkichi Kingdom", "Twitch", 1 },
{ 7, 4, "@moonrelay", "Moonrelay", "Twitch", 1 },
{ 8, 5, "@hoshimimiyu", "Hoshimi Miyu", "Twitch", 2 },
{ 9, 6, "@kurainu", "Kurainu 3D Live", "Twitch", 2 },
{ 10, 7, "@pyonkichikingdom", "Pyonkichi Kingdom", "Twitch", 2 },
{ 11, 8, "@aoisakura", "Aoi Sakura", "YouTube", 3 },
{ 12, 9, "@starbyte", "Starbyte", "Twitch", 3 },
{ 13, 10, "@tenshivox", "Tenshi Vox", "Twitch", 4 }
});
migrationBuilder.InsertData(
table: "Nominations",
columns: new[] { "Id", "CandidateId", "CandidateText", "CategoryId", "CreatedAt", "SeasonId", "SubmittedByTwitchId" },
values: new object[,]
{
{ 1, null, "Hoshimi Miyu", 1, new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), 1, "twitch_hoshi" },
{ 2, null, "Kurainu 3D Live", 2, new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), 1, "twitch_kurainu" }
});
migrationBuilder.InsertData(
table: "Results",
columns: new[] { "Id", "CandidateId", "CategoryName", "SeasonId" },
values: new object[,]
{
{ 1, 8, "VTuber des Jahres", 2 },
{ 2, 9, "Bestes Live Event", 2 },
{ 3, 10, "Clip des Jahres", 2 },
{ 4, 11, "VTuber des Jahres", 3 },
{ 5, 12, "Clip des Jahres", 3 },
{ 6, 13, "VTuber des Jahres", 4 }
});
migrationBuilder.InsertData(
table: "VoteEntries",
columns: new[] { "Id", "BallotId", "CandidateId", "CategoryId" },
values: new object[,]
{
{ 1, 1, 1, 1 },
{ 2, 1, 4, 2 },
{ 3, 2, 2, 1 },
{ 4, 2, 6, 3 }
});
migrationBuilder.CreateIndex(
name: "IX_Candidates_CategoryId",
table: "Candidates",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Candidates_SeasonId",
table: "Candidates",
column: "SeasonId");
migrationBuilder.CreateIndex(
name: "IX_Categories_SeasonId_Slug",
table: "Categories",
columns: new[] { "SeasonId", "Slug" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Nominations_CandidateId",
table: "Nominations",
column: "CandidateId");
migrationBuilder.CreateIndex(
name: "IX_Nominations_CategoryId",
table: "Nominations",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Nominations_SeasonId",
table: "Nominations",
column: "SeasonId");
migrationBuilder.CreateIndex(
name: "IX_Results_CandidateId",
table: "Results",
column: "CandidateId");
migrationBuilder.CreateIndex(
name: "IX_Results_SeasonId",
table: "Results",
column: "SeasonId");
migrationBuilder.CreateIndex(
name: "IX_Seasons_Year",
table: "Seasons",
column: "Year",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_VoteBallots_SeasonId",
table: "VoteBallots",
column: "SeasonId");
migrationBuilder.CreateIndex(
name: "IX_VoteEntries_BallotId",
table: "VoteEntries",
column: "BallotId");
migrationBuilder.CreateIndex(
name: "IX_VoteEntries_CandidateId",
table: "VoteEntries",
column: "CandidateId");
migrationBuilder.CreateIndex(
name: "IX_VoteEntries_CategoryId",
table: "VoteEntries",
column: "CategoryId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Nominations");
migrationBuilder.DropTable(
name: "Results");
migrationBuilder.DropTable(
name: "VoteEntries");
migrationBuilder.DropTable(
name: "Candidates");
migrationBuilder.DropTable(
name: "VoteBallots");
migrationBuilder.DropTable(
name: "Categories");
migrationBuilder.DropTable(
name: "Seasons");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddClipReviewWorkflow : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE IF EXISTS "ClipSubmissions"
ADD COLUMN IF NOT EXISTS "ReviewNote" character varying(500) NULL;
ALTER TABLE IF EXISTS "ClipSubmissions"
ADD COLUMN IF NOT EXISTS "ReviewedByTwitchId" character varying(120) NULL;
ALTER TABLE IF EXISTS "ClipSubmissions"
ADD COLUMN IF NOT EXISTS "ReviewedAt" timestamp with time zone NULL;
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE IF EXISTS "ClipSubmissions"
DROP COLUMN IF EXISTS "ReviewNote";
ALTER TABLE IF EXISTS "ClipSubmissions"
DROP COLUMN IF EXISTS "ReviewedByTwitchId";
ALTER TABLE IF EXISTS "ClipSubmissions"
DROP COLUMN IF EXISTS "ReviewedAt";
""");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,95 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddNominationReviewWorkflow : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Nominations_SeasonId",
table: "Nominations");
migrationBuilder.AddColumn<string>(
name: "ReviewNote",
table: "Nominations",
type: "character varying(500)",
maxLength: 500,
nullable: true);
migrationBuilder.AddColumn<DateTimeOffset>(
name: "ReviewedAt",
table: "Nominations",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ReviewedByTwitchId",
table: "Nominations",
type: "character varying(120)",
maxLength: 120,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Status",
table: "Nominations",
type: "character varying(20)",
maxLength: 20,
nullable: false,
defaultValue: "");
migrationBuilder.UpdateData(
table: "Nominations",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "ReviewNote", "ReviewedAt", "ReviewedByTwitchId", "Status" },
values: new object[] { null, null, null, "pending" });
migrationBuilder.UpdateData(
table: "Nominations",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "ReviewNote", "ReviewedAt", "ReviewedByTwitchId", "Status" },
values: new object[] { null, null, null, "pending" });
migrationBuilder.CreateIndex(
name: "IX_Nominations_SeasonId_Status",
table: "Nominations",
columns: new[] { "SeasonId", "Status" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Nominations_SeasonId_Status",
table: "Nominations");
migrationBuilder.DropColumn(
name: "ReviewNote",
table: "Nominations");
migrationBuilder.DropColumn(
name: "ReviewedAt",
table: "Nominations");
migrationBuilder.DropColumn(
name: "ReviewedByTwitchId",
table: "Nominations");
migrationBuilder.DropColumn(
name: "Status",
table: "Nominations");
migrationBuilder.CreateIndex(
name: "IX_Nominations_SeasonId",
table: "Nominations",
column: "SeasonId");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddAwardResultCategoryLock : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Results_SeasonId",
table: "Results");
migrationBuilder.AddColumn<int>(
name: "CategoryId",
table: "Results",
type: "integer",
nullable: true);
migrationBuilder.Sql(
"""
UPDATE "Results" AS r
SET "CategoryId" = c."Id"
FROM "Categories" AS c
WHERE r."SeasonId" = c."SeasonId"
AND lower(trim(r."CategoryName")) = lower(trim(c."Name"));
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM "Results" WHERE "CategoryId" IS NULL) THEN
RAISE EXCEPTION 'Could not backfill CategoryId for one or more rows in Results.';
END IF;
END $$;
""");
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "Results",
type: "integer",
nullable: false,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.CreateIndex(
name: "IX_Results_CategoryId",
table: "Results",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Results_SeasonId_CategoryId",
table: "Results",
columns: new[] { "SeasonId", "CategoryId" },
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_Results_Categories_CategoryId",
table: "Results",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Results_Categories_CategoryId",
table: "Results");
migrationBuilder.DropIndex(
name: "IX_Results_CategoryId",
table: "Results");
migrationBuilder.DropIndex(
name: "IX_Results_SeasonId_CategoryId",
table: "Results");
migrationBuilder.DropColumn(
name: "CategoryId",
table: "Results");
migrationBuilder.CreateIndex(
name: "IX_Results_SeasonId",
table: "Results",
column: "SeasonId");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddSeasonShowStreamUrl : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ShowStreamUrl",
table: "Seasons",
type: "character varying(400)",
maxLength: 400,
nullable: false,
defaultValue: "");
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 1,
column: "ShowStreamUrl",
value: "https://twitch.tv/jayuhime");
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 2,
column: "ShowStreamUrl",
value: "https://twitch.tv/jayuhime");
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 3,
column: "ShowStreamUrl",
value: "https://youtube.com/c/Jayuhime");
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 4,
column: "ShowStreamUrl",
value: "https://twitch.tv/jayuhime");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ShowStreamUrl",
table: "Seasons");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddSiteSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SiteSettings",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
HostDisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
HostTagline = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
NewsletterUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
PrivacyEmail = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
ImprintUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
ContactUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
SponsorsUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
SocialLinksJson = table.Column<string>(type: "text", nullable: false),
FaqJson = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SiteSettings", x => x.Id);
});
migrationBuilder.InsertData(
table: "SiteSettings",
columns: new[] { "Id", "ContactUrl", "FaqJson", "HostDisplayName", "HostTagline", "ImprintUrl", "NewsletterUrl", "PrivacyEmail", "SocialLinksJson", "SponsorsUrl" },
values: new object[] { 1, "https://vtuber-star-awards.de/kontakt", "[{\"question\":\"Wer darf mitmachen?\",\"answer\":\"Jede:r mit einem Twitch-Account. Einmal einloggen gen\\u00FCgt \\u2013 kein extra Konto, kein Papierkram.\"},{\"question\":\"Wie werden die Gewinner bestimmt?\",\"answer\":\"Komplett durch eure Stimmen. Die Community entscheidet, wer auf die B\\u00FChne darf.\"},{\"question\":\"Kann ich meine Wahl noch \\u00E4ndern?\",\"answer\":\"Ja. Bis zum Ende der Voting-Phase kannst du Nominierungen und Stimmen anpassen.\"},{\"question\":\"Wann und wo findet die Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und live auf den verkn\\u00FCpften Plattformen \\u00FCbertragen.\"}]", "Jayuhime", "VTuber & Award Host", "https://vtuber-star-awards.de/impressum", "https://vtuber-star-awards.de/newsletter", "datenschutz@vtuber-star-awards.de", "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\"},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\"},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\"},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\"},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\"}]", "https://vtuber-star-awards.de/partner" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SiteSettings");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddPrivacyPolicyContentMetadata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "PrivacyPolicyContent",
table: "SiteSettings",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<DateTimeOffset>(
name: "PrivacyPolicyUpdatedAt",
table: "SiteSettings",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PrivacyPolicyUpdatedBy",
table: "SiteSettings",
type: "character varying(120)",
maxLength: 120,
nullable: true);
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "PrivacyPolicyContent", "PrivacyPolicyUpdatedAt", "PrivacyPolicyUpdatedBy" },
values: new object[] { "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "seed" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PrivacyPolicyContent",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "PrivacyPolicyUpdatedAt",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "PrivacyPolicyUpdatedBy",
table: "SiteSettings");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddOperationalSiteSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DemoLoginDisplayName",
table: "SiteSettings",
type: "character varying(120)",
maxLength: 120,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "DemoLoginEmail",
table: "SiteSettings",
type: "character varying(180)",
maxLength: 180,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<bool>(
name: "DemoLoginEnabled",
table: "SiteSettings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "DemoLoginManagedByDatabase",
table: "SiteSettings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "DemoLoginPasswordHash",
table: "SiteSettings",
type: "character varying(120)",
maxLength: 120,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "DemoLoginPasswordSalt",
table: "SiteSettings",
type: "character varying(80)",
maxLength: 80,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "DemoLoginTwitchUserId",
table: "SiteSettings",
type: "character varying(120)",
maxLength: 120,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "MaintenanceMessage",
table: "SiteSettings",
type: "character varying(600)",
maxLength: 600,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<bool>(
name: "MaintenanceModeEnabled",
table: "SiteSettings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "MaintenanceTitle",
table: "SiteSettings",
type: "character varying(120)",
maxLength: 120,
nullable: false,
defaultValue: "");
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "DemoLoginDisplayName", "DemoLoginEmail", "DemoLoginEnabled", "DemoLoginManagedByDatabase", "DemoLoginPasswordHash", "DemoLoginPasswordSalt", "DemoLoginTwitchUserId", "MaintenanceMessage", "MaintenanceModeEnabled", "MaintenanceTitle" },
values: new object[] { "Jayuhime Admin", "", false, false, "", "", "jayuhime_admin", "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", false, "Sternenpause" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DemoLoginDisplayName",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "DemoLoginEmail",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "DemoLoginEnabled",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "DemoLoginManagedByDatabase",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "DemoLoginPasswordHash",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "DemoLoginPasswordSalt",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "DemoLoginTwitchUserId",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "MaintenanceMessage",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "MaintenanceModeEnabled",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "MaintenanceTitle",
table: "SiteSettings");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddSeasonShowStartsAt : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "Seasons"
ADD COLUMN IF NOT EXISTS "ShowStartsAt" time without time zone NOT NULL DEFAULT TIME '20:00:00';
""");
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 1,
column: "ShowStartsAt",
value: new TimeOnly(20, 0, 0));
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 2,
column: "ShowStartsAt",
value: new TimeOnly(20, 0, 0));
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 3,
column: "ShowStartsAt",
value: new TimeOnly(20, 0, 0));
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 4,
column: "ShowStartsAt",
value: new TimeOnly(20, 0, 0));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ShowStartsAt",
table: "Seasons");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddClipCandidateLink : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE IF EXISTS "ClipSubmissions"
ADD COLUMN IF NOT EXISTS "CandidateId" integer NULL;
DO $$
BEGIN
IF to_regclass('"ClipSubmissions"') IS NOT NULL THEN
CREATE INDEX IF NOT EXISTS "IX_ClipSubmissions_CandidateId"
ON "ClipSubmissions" ("CandidateId");
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'FK_ClipSubmissions_Candidates_CandidateId'
) THEN
ALTER TABLE "ClipSubmissions"
ADD CONSTRAINT "FK_ClipSubmissions_Candidates_CandidateId"
FOREIGN KEY ("CandidateId") REFERENCES "Candidates" ("Id")
ON DELETE SET NULL;
END IF;
END IF;
END $$;
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE IF EXISTS "ClipSubmissions"
DROP CONSTRAINT IF EXISTS "FK_ClipSubmissions_Candidates_CandidateId";
DROP INDEX IF EXISTS "IX_ClipSubmissions_CandidateId";
ALTER TABLE IF EXISTS "ClipSubmissions"
DROP COLUMN IF EXISTS "CandidateId";
""");
}
}
}
@@ -0,0 +1,34 @@
using Backend.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
[DbContext(typeof(AwardsDbContext))]
[Migration("20260624150500_AddRiskFlagReviewNote")]
public partial class AddRiskFlagReviewNote : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE IF EXISTS "RiskFlags"
ADD COLUMN IF NOT EXISTS "ReviewNote" character varying(500) NULL;
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE IF EXISTS "RiskFlags"
DROP COLUMN IF EXISTS "ReviewNote";
""");
}
}
}
@@ -0,0 +1,39 @@
using Backend.Data;
using Backend.Services;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
[DbContext(typeof(AwardsDbContext))]
[Migration("20260624162000_AddRiskRulesJson")]
public partial class AddRiskRulesJson : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
$"""
ALTER TABLE IF EXISTS "SiteSettings"
ADD COLUMN IF NOT EXISTS "RiskRulesJson" text NOT NULL DEFAULT '{RiskRuleSettings.Serialize(RiskRuleSettings.Defaults).Replace("'", "''")}';
UPDATE "SiteSettings"
SET "RiskRulesJson" = '{RiskRuleSettings.Serialize(RiskRuleSettings.Defaults).Replace("'", "''")}'
WHERE "RiskRulesJson" IS NULL OR btrim("RiskRulesJson") = '' OR "RiskRulesJson" = '[]';
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE IF EXISTS "SiteSettings"
DROP COLUMN IF EXISTS "RiskRulesJson";
""");
}
}
}
@@ -0,0 +1,54 @@
using Backend.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
[DbContext(typeof(AwardsDbContext))]
[Migration("20260625110000_AddFooterPageContent")]
public partial class AddFooterPageContent : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
var imprintContent = SeedCatalog.DefaultImprintContent.Replace("'", "''");
var contactContent = SeedCatalog.DefaultContactContent.Replace("'", "''");
var sponsorsContent = SeedCatalog.DefaultSponsorsContent.Replace("'", "''");
migrationBuilder.Sql(
$"""
ALTER TABLE IF EXISTS "SiteSettings"
ADD COLUMN IF NOT EXISTS "ImprintContent" text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "ContactContent" text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS "SponsorsContent" text NOT NULL DEFAULT '';
UPDATE "SiteSettings"
SET "ImprintContent" = '{imprintContent}'
WHERE "ImprintContent" IS NULL OR btrim("ImprintContent") = '';
UPDATE "SiteSettings"
SET "ContactContent" = '{contactContent}'
WHERE "ContactContent" IS NULL OR btrim("ContactContent") = '';
UPDATE "SiteSettings"
SET "SponsorsContent" = '{sponsorsContent}'
WHERE "SponsorsContent" IS NULL OR btrim("SponsorsContent") = '';
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE IF EXISTS "SiteSettings"
DROP COLUMN IF EXISTS "ImprintContent",
DROP COLUMN IF EXISTS "ContactContent",
DROP COLUMN IF EXISTS "SponsorsContent";
""");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,79 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddTeamManagement : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TeamMembers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Login = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
DisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Role = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
PasswordHash = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
PasswordSalt = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
MustChangePassword = table.Column<bool>(type: "boolean", nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false),
CreatedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
UpdatedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LastLoginAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
PasswordResetAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_TeamMembers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "TeamRolePermissions",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Role = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
PermissionsJson = table.Column<string>(type: "text", nullable: false),
UpdatedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TeamRolePermissions", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_TeamMembers_Login",
table: "TeamMembers",
column: "Login",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_TeamRolePermissions_Role",
table: "TeamRolePermissions",
column: "Role",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TeamMembers");
migrationBuilder.DropTable(
name: "TeamRolePermissions");
}
}
}
@@ -0,0 +1,66 @@
using System;
using Backend.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
[DbContext(typeof(AwardsDbContext))]
[Migration("20260625153000_AddCreatorRoleAndTeamTwitchBinding")]
public partial class AddCreatorRoleAndTeamTwitchBinding : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "BoundTwitchDisplayName",
table: "TeamMembers",
type: "character varying(120)",
maxLength: 120,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "BoundTwitchUserId",
table: "TeamMembers",
type: "character varying(120)",
maxLength: 120,
nullable: true);
migrationBuilder.AddColumn<DateTimeOffset>(
name: "TwitchBoundAt",
table: "TeamMembers",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_TeamMembers_BoundTwitchUserId",
table: "TeamMembers",
column: "BoundTwitchUserId",
unique: true,
filter: "\"BoundTwitchUserId\" IS NOT NULL");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_TeamMembers_BoundTwitchUserId",
table: "TeamMembers");
migrationBuilder.DropColumn(
name: "BoundTwitchDisplayName",
table: "TeamMembers");
migrationBuilder.DropColumn(
name: "BoundTwitchUserId",
table: "TeamMembers");
migrationBuilder.DropColumn(
name: "TwitchBoundAt",
table: "TeamMembers");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,135 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddVoteBallotSubmitterUniqueIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_VoteBallots_SeasonId",
table: "VoteBallots");
migrationBuilder.AddColumn<bool>(
name: "TwitchAuthManagedByDatabase",
table: "SiteSettings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "TwitchClientId",
table: "SiteSettings",
type: "character varying(120)",
maxLength: 120,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "TwitchClientSecret",
table: "SiteSettings",
type: "character varying(180)",
maxLength: 180,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "TwitchRedirectUri",
table: "SiteSettings",
type: "character varying(400)",
maxLength: 400,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "TwitchScope",
table: "SiteSettings",
type: "character varying(300)",
maxLength: 300,
nullable: false,
defaultValue: "");
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "TwitchAuthManagedByDatabase", "TwitchClientId", "TwitchClientSecret", "TwitchRedirectUri", "TwitchScope" },
values: new object[] { false, "", "", "", "" });
migrationBuilder.Sql("""
DELETE FROM "VoteEntries"
WHERE "BallotId" IN (
SELECT "Id"
FROM (
SELECT
"Id",
ROW_NUMBER() OVER (
PARTITION BY "SeasonId", "SubmittedByTwitchId"
ORDER BY "SubmittedAt" DESC, "Id" DESC
) AS duplicate_rank
FROM "VoteBallots"
) ranked_ballots
WHERE duplicate_rank > 1
);
DELETE FROM "VoteBallots"
WHERE "Id" IN (
SELECT "Id"
FROM (
SELECT
"Id",
ROW_NUMBER() OVER (
PARTITION BY "SeasonId", "SubmittedByTwitchId"
ORDER BY "SubmittedAt" DESC, "Id" DESC
) AS duplicate_rank
FROM "VoteBallots"
) ranked_ballots
WHERE duplicate_rank > 1
);
""");
migrationBuilder.CreateIndex(
name: "IX_VoteBallots_SeasonId_SubmittedByTwitchId",
table: "VoteBallots",
columns: new[] { "SeasonId", "SubmittedByTwitchId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_VoteBallots_SeasonId_SubmittedByTwitchId",
table: "VoteBallots");
migrationBuilder.DropColumn(
name: "TwitchAuthManagedByDatabase",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "TwitchClientId",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "TwitchClientSecret",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "TwitchRedirectUri",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "TwitchScope",
table: "SiteSettings");
migrationBuilder.CreateIndex(
name: "IX_VoteBallots_SeasonId",
table: "VoteBallots",
column: "SeasonId");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,177 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddCandidatePreparationFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "AcceptanceNote",
table: "Candidates",
type: "character varying(500)",
maxLength: 500,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "AcceptanceStatus",
table: "Candidates",
type: "character varying(30)",
maxLength: 30,
nullable: false,
defaultValue: "open");
migrationBuilder.AddColumn<string>(
name: "ClipCompilationPlatform",
table: "Candidates",
type: "character varying(40)",
maxLength: 40,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ClipCompilationTitle",
table: "Candidates",
type: "character varying(200)",
maxLength: 200,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ClipCompilationUrl",
table: "Candidates",
type: "character varying(500)",
maxLength: 500,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ClipEmbedStatus",
table: "Candidates",
type: "character varying(30)",
maxLength: 30,
nullable: false,
defaultValue: "unchecked");
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 3,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 4,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 5,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 6,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 7,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 8,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 9,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 10,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 11,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 12,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 13,
columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" },
values: new object[] { null, "open", null, null, null, "unchecked" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AcceptanceNote",
table: "Candidates");
migrationBuilder.DropColumn(
name: "AcceptanceStatus",
table: "Candidates");
migrationBuilder.DropColumn(
name: "ClipCompilationPlatform",
table: "Candidates");
migrationBuilder.DropColumn(
name: "ClipCompilationTitle",
table: "Candidates");
migrationBuilder.DropColumn(
name: "ClipCompilationUrl",
table: "Candidates");
migrationBuilder.DropColumn(
name: "ClipEmbedStatus",
table: "Candidates");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddWorkflowRulesJson : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
ALTER TABLE "SiteSettings"
ADD COLUMN IF NOT EXISTS "WorkflowRulesJson" text NOT NULL DEFAULT '[]';
""");
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
column: "WorkflowRulesJson",
value: "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"}]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
ALTER TABLE "SiteSettings"
DROP COLUMN IF EXISTS "WorkflowRulesJson";
""");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddOptionalClipFeatureSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
ALTER TABLE "SiteSettings"
ADD COLUMN IF NOT EXISTS "ClipReviewEnabled" boolean NOT NULL DEFAULT true;
""");
migrationBuilder.Sql("""
ALTER TABLE "SiteSettings"
ADD COLUMN IF NOT EXISTS "ClipSubmissionDisabledMessage" character varying(240) NOT NULL DEFAULT 'Clip-Einreichungen sind aktuell geschlossen.';
""");
migrationBuilder.Sql("""
ALTER TABLE "SiteSettings"
ADD COLUMN IF NOT EXISTS "ClipSubmissionsEnabled" boolean NOT NULL DEFAULT false;
""");
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "ClipReviewEnabled", "ClipSubmissionDisabledMessage" },
values: new object[] { true, "Clip-Einreichungen sind aktuell geschlossen." });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
ALTER TABLE "SiteSettings"
DROP COLUMN IF EXISTS "ClipReviewEnabled";
""");
migrationBuilder.Sql("""
ALTER TABLE "SiteSettings"
DROP COLUMN IF EXISTS "ClipSubmissionDisabledMessage";
""");
migrationBuilder.Sql("""
ALTER TABLE "SiteSettings"
DROP COLUMN IF EXISTS "ClipSubmissionsEnabled";
""");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddNominationLinkBlacklist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "NominationLinkBlacklistJson",
table: "SiteSettings",
type: "text",
nullable: false,
defaultValue: "[]");
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
column: "NominationLinkBlacklistJson",
value: "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "NominationLinkBlacklistJson",
table: "SiteSettings");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,152 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddClipAdminMenuVisible : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
ALTER TABLE "SiteSettings"
ADD COLUMN IF NOT EXISTS "ClipAdminMenuVisible" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "SiteSettings"
ADD COLUMN IF NOT EXISTS "ShowactApplicationDisabledMessage" character varying(240) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings"
ADD COLUMN IF NOT EXISTS "ShowactApplicationsEnabled" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "SiteSettings"
ADD COLUMN IF NOT EXISTS "ShowactsContent" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings"
ADD COLUMN IF NOT EXISTS "ShowactsUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings"
ADD COLUMN IF NOT EXISTS "SponsorsVisible" boolean NOT NULL DEFAULT TRUE;
""");
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS "ShowactApplications" (
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
"SeasonId" integer NOT NULL,
"ArtistName" character varying(120) NOT NULL,
"ContactEmail" character varying(180) NOT NULL,
"ContactDiscord" character varying(120) NOT NULL,
"PlatformUrl" character varying(500) NOT NULL,
"PerformanceType" character varying(80) NOT NULL,
"Description" character varying(1000) NOT NULL,
"TechnicalNotes" character varying(1000) NOT NULL,
"ReferenceUrl" character varying(500) NOT NULL,
"Status" character varying(20) NOT NULL,
"ReviewNote" character varying(500) NULL,
"ReviewedByTwitchId" character varying(120) NULL,
"CreatedFromIp" character varying(80) NOT NULL,
"UserAgent" character varying(400) NOT NULL,
"CreatedAt" timestamp with time zone NOT NULL,
"ReviewedAt" timestamp with time zone NULL
);
CREATE TABLE IF NOT EXISTS "Sponsors" (
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
"SeasonId" integer NOT NULL,
"Name" character varying(120) NOT NULL,
"WebsiteUrl" character varying(500) NOT NULL,
"LogoUrl" character varying(500) NOT NULL,
"Description" character varying(500) NOT NULL,
"Tier" character varying(80) NOT NULL,
"SortOrder" integer NOT NULL,
"IsVisible" boolean NOT NULL,
"CreatedAt" timestamp with time zone NOT NULL,
"UpdatedAt" timestamp with time zone NULL
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'FK_ShowactApplications_Seasons_SeasonId'
) THEN
ALTER TABLE "ShowactApplications"
ADD CONSTRAINT "FK_ShowactApplications_Seasons_SeasonId"
FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id")
ON DELETE CASCADE;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'FK_Sponsors_Seasons_SeasonId'
) THEN
ALTER TABLE "Sponsors"
ADD CONSTRAINT "FK_Sponsors_Seasons_SeasonId"
FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id")
ON DELETE CASCADE;
END IF;
END $$;
""");
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "ClipAdminMenuVisible", "ShowactApplicationDisabledMessage", "ShowactsContent", "ShowactsUrl", "SponsorsVisible", "WorkflowRulesJson" },
values: new object[] { true, "Showact-Bewerbungen sind aktuell geschlossen.", "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", "https://vtuber-star-awards.de/showacts", true, "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" });
migrationBuilder.Sql("""
CREATE INDEX IF NOT EXISTS "IX_ShowactApplications_SeasonId_Status"
ON "ShowactApplications" ("SeasonId", "Status");
CREATE INDEX IF NOT EXISTS "IX_Sponsors_SeasonId_IsVisible_SortOrder"
ON "Sponsors" ("SeasonId", "IsVisible", "SortOrder");
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DROP TABLE IF EXISTS "ShowactApplications";
DROP TABLE IF EXISTS "Sponsors";
""");
migrationBuilder.Sql("""
ALTER TABLE "SiteSettings"
DROP COLUMN IF EXISTS "ClipAdminMenuVisible";
ALTER TABLE "SiteSettings"
DROP COLUMN IF EXISTS "ShowactApplicationDisabledMessage";
ALTER TABLE "SiteSettings"
DROP COLUMN IF EXISTS "ShowactApplicationsEnabled";
ALTER TABLE "SiteSettings"
DROP COLUMN IF EXISTS "ShowactsContent";
ALTER TABLE "SiteSettings"
DROP COLUMN IF EXISTS "ShowactsUrl";
ALTER TABLE "SiteSettings"
DROP COLUMN IF EXISTS "SponsorsVisible";
""");
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
column: "WorkflowRulesJson",
value: "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"}]");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddShareUrls : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ShareDiscordUrl",
table: "SiteSettings",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "ShareXUrl",
table: "SiteSettings",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "ShareDiscordUrl", "ShareXUrl" },
values: new object[] { "", "" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ShareDiscordUrl",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "ShareXUrl",
table: "SiteSettings");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddShowactDynamicForm : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ShowactFormSchemaJson",
table: "SiteSettings",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "FieldResponsesJson",
table: "ShowactApplications",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
column: "ShowactFormSchemaJson",
value: "[]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ShowactFormSchemaJson",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "FieldResponsesJson",
table: "ShowactApplications");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,108 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddCategoryViewerRanges : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ViewerRangeMax",
table: "Categories",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "ViewerRangeMin",
table: "Categories",
type: "integer",
nullable: true);
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "ViewerRangeMax", "ViewerRangeMin" },
values: new object[] { null, null });
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "ViewerRangeMax", "ViewerRangeMin" },
values: new object[] { null, null });
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "Id",
keyValue: 3,
columns: new[] { "ViewerRangeMax", "ViewerRangeMin" },
values: new object[] { null, null });
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "Id",
keyValue: 4,
columns: new[] { "ViewerRangeMax", "ViewerRangeMin" },
values: new object[] { null, null });
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "Id",
keyValue: 5,
columns: new[] { "ViewerRangeMax", "ViewerRangeMin" },
values: new object[] { null, null });
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "Id",
keyValue: 6,
columns: new[] { "ViewerRangeMax", "ViewerRangeMin" },
values: new object[] { null, null });
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "Id",
keyValue: 7,
columns: new[] { "ViewerRangeMax", "ViewerRangeMin" },
values: new object[] { null, null });
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "Id",
keyValue: 8,
columns: new[] { "ViewerRangeMax", "ViewerRangeMin" },
values: new object[] { null, null });
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "Id",
keyValue: 9,
columns: new[] { "ViewerRangeMax", "ViewerRangeMin" },
values: new object[] { null, null });
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "Id",
keyValue: 10,
columns: new[] { "ViewerRangeMax", "ViewerRangeMin" },
values: new object[] { null, null });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ViewerRangeMax",
table: "Categories");
migrationBuilder.DropColumn(
name: "ViewerRangeMin",
table: "Categories");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddSessionIdleTimeoutSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "SessionIdleTimeoutHours",
table: "SiteSettings",
type: "integer",
nullable: false,
defaultValue: 3);
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
column: "SessionIdleTimeoutHours",
value: 3);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SessionIdleTimeoutHours",
table: "SiteSettings");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddSeasonSubcategoryTemplates : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SubcategoryTemplatesJson",
table: "Seasons",
type: "text",
nullable: false,
defaultValue: "[]");
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 1,
column: "SubcategoryTemplatesJson",
value: "[]");
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 2,
column: "SubcategoryTemplatesJson",
value: "[]");
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 3,
column: "SubcategoryTemplatesJson",
value: "[]");
migrationBuilder.UpdateData(
table: "Seasons",
keyColumn: "Id",
keyValue: 4,
column: "SubcategoryTemplatesJson",
value: "[]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SubcategoryTemplatesJson",
table: "Seasons");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,388 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddNominationGroupTrackerIdentity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Nominations_Categories_CategoryId",
table: "Nominations");
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "Nominations",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AddColumn<int>(
name: "AvgViewers",
table: "Nominations",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "CategoryGroupName",
table: "Nominations",
type: "character varying(80)",
maxLength: 80,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "ResolvedChannel",
table: "Nominations",
type: "character varying(120)",
maxLength: 120,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ResolvedPlatform",
table: "Nominations",
type: "character varying(40)",
maxLength: 40,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "StreamerIdentityId",
table: "Nominations",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "SuggestedCategoryId",
table: "Nominations",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<DateTimeOffset>(
name: "TrackerCheckedAt",
table: "Nominations",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "TrackerStatus",
table: "Nominations",
type: "character varying(40)",
maxLength: 40,
nullable: false,
defaultValue: "pending");
migrationBuilder.AddColumn<int>(
name: "NominationTally",
table: "Candidates",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "StreamerIdentityId",
table: "Candidates",
type: "integer",
nullable: true);
migrationBuilder.CreateTable(
name: "StreamerIdentities",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Platform = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
Login = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
NormalizedKey = table.Column<string>(type: "character varying(180)", maxLength: 180, nullable: false),
DisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
ProfileUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
LastResolvedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_StreamerIdentities", x => x.Id);
});
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 1,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 2,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 3,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 4,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 5,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 6,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 7,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 8,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 9,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 10,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 11,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 12,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Candidates",
keyColumn: "Id",
keyValue: 13,
column: "StreamerIdentityId",
value: null);
migrationBuilder.UpdateData(
table: "Nominations",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "AvgViewers", "CategoryGroupName", "ResolvedChannel", "ResolvedPlatform", "StreamerIdentityId", "SuggestedCategoryId", "TrackerCheckedAt", "TrackerStatus" },
values: new object[] { null, "", null, null, null, null, null, "pending" });
migrationBuilder.UpdateData(
table: "Nominations",
keyColumn: "Id",
keyValue: 2,
columns: new[] { "AvgViewers", "CategoryGroupName", "ResolvedChannel", "ResolvedPlatform", "StreamerIdentityId", "SuggestedCategoryId", "TrackerCheckedAt", "TrackerStatus" },
values: new object[] { null, "", null, null, null, null, null, "pending" });
migrationBuilder.Sql("""
UPDATE "Nominations" n
SET "CategoryGroupName" = c."GroupName"
FROM "Categories" c
WHERE n."CategoryId" = c."Id"
AND COALESCE(n."CategoryGroupName", '') = '';
""");
migrationBuilder.CreateIndex(
name: "IX_Nominations_SeasonId_CategoryGroupName_Status",
table: "Nominations",
columns: new[] { "SeasonId", "CategoryGroupName", "Status" });
migrationBuilder.CreateIndex(
name: "IX_Nominations_SeasonId_StreamerIdentityId_CategoryGroupName",
table: "Nominations",
columns: new[] { "SeasonId", "StreamerIdentityId", "CategoryGroupName" });
migrationBuilder.CreateIndex(
name: "IX_Nominations_StreamerIdentityId",
table: "Nominations",
column: "StreamerIdentityId");
migrationBuilder.CreateIndex(
name: "IX_Nominations_SuggestedCategoryId",
table: "Nominations",
column: "SuggestedCategoryId");
migrationBuilder.CreateIndex(
name: "IX_Candidates_StreamerIdentityId",
table: "Candidates",
column: "StreamerIdentityId");
migrationBuilder.CreateIndex(
name: "IX_StreamerIdentities_NormalizedKey",
table: "StreamerIdentities",
column: "NormalizedKey",
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_Candidates_StreamerIdentities_StreamerIdentityId",
table: "Candidates",
column: "StreamerIdentityId",
principalTable: "StreamerIdentities",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Nominations_Categories_CategoryId",
table: "Nominations",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Nominations_Categories_SuggestedCategoryId",
table: "Nominations",
column: "SuggestedCategoryId",
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Nominations_StreamerIdentities_StreamerIdentityId",
table: "Nominations",
column: "StreamerIdentityId",
principalTable: "StreamerIdentities",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Candidates_StreamerIdentities_StreamerIdentityId",
table: "Candidates");
migrationBuilder.DropForeignKey(
name: "FK_Nominations_Categories_CategoryId",
table: "Nominations");
migrationBuilder.DropForeignKey(
name: "FK_Nominations_Categories_SuggestedCategoryId",
table: "Nominations");
migrationBuilder.DropForeignKey(
name: "FK_Nominations_StreamerIdentities_StreamerIdentityId",
table: "Nominations");
migrationBuilder.DropTable(
name: "StreamerIdentities");
migrationBuilder.DropIndex(
name: "IX_Nominations_SeasonId_CategoryGroupName_Status",
table: "Nominations");
migrationBuilder.DropIndex(
name: "IX_Nominations_SeasonId_StreamerIdentityId_CategoryGroupName",
table: "Nominations");
migrationBuilder.DropIndex(
name: "IX_Nominations_StreamerIdentityId",
table: "Nominations");
migrationBuilder.DropIndex(
name: "IX_Nominations_SuggestedCategoryId",
table: "Nominations");
migrationBuilder.DropIndex(
name: "IX_Candidates_StreamerIdentityId",
table: "Candidates");
migrationBuilder.DropColumn(
name: "AvgViewers",
table: "Nominations");
migrationBuilder.DropColumn(
name: "CategoryGroupName",
table: "Nominations");
migrationBuilder.DropColumn(
name: "ResolvedChannel",
table: "Nominations");
migrationBuilder.DropColumn(
name: "ResolvedPlatform",
table: "Nominations");
migrationBuilder.DropColumn(
name: "StreamerIdentityId",
table: "Nominations");
migrationBuilder.DropColumn(
name: "SuggestedCategoryId",
table: "Nominations");
migrationBuilder.DropColumn(
name: "TrackerCheckedAt",
table: "Nominations");
migrationBuilder.DropColumn(
name: "TrackerStatus",
table: "Nominations");
migrationBuilder.DropColumn(
name: "NominationTally",
table: "Candidates");
migrationBuilder.DropColumn(
name: "StreamerIdentityId",
table: "Candidates");
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "Nominations",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_Nominations_Categories_CategoryId",
table: "Nominations",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddShowactApplicationSchedule : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateOnly>(
name: "ShowactApplicationEndsAt",
table: "SiteSettings",
type: "date",
nullable: true);
migrationBuilder.AddColumn<DateOnly>(
name: "ShowactApplicationStartsAt",
table: "SiteSettings",
type: "date",
nullable: true);
migrationBuilder.UpdateData(
table: "SiteSettings",
keyColumn: "Id",
keyValue: 1,
columns: new[] { "ShowactApplicationEndsAt", "ShowactApplicationStartsAt" },
values: new object[] { null, null });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ShowactApplicationEndsAt",
table: "SiteSettings");
migrationBuilder.DropColumn(
name: "ShowactApplicationStartsAt",
table: "SiteSettings");
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddSeasonWorkflowRulesJson : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "WorkflowRulesJson",
table: "Seasons",
type: "text",
nullable: false,
defaultValue: "[]");
migrationBuilder.Sql(
"""
UPDATE "Seasons"
SET "WorkflowRulesJson" = COALESCE(NULLIF((SELECT "WorkflowRulesJson" FROM "SiteSettings" WHERE "Id" = 1), ''), '[]')
WHERE COALESCE("WorkflowRulesJson", '') = '';
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "WorkflowRulesJson",
table: "Seasons");
}
}
}
@@ -1,795 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class InitialCleanBaseline : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AdminAuditEntries",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
AdminTwitchUserId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
ActionType = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
EntityType = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
EntityId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Summary = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: false),
MetadataJson = table.Column<string>(type: "text", nullable: false),
CreatedFromIp = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
UserAgent = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AdminAuditEntries", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Seasons",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Year = table.Column<int>(type: "integer", nullable: false),
Name = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
IsDemo = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
IsCurrent = table.Column<bool>(type: "boolean", nullable: false),
IsCommunityOnly = table.Column<bool>(type: "boolean", nullable: false),
CurrentPhase = table.Column<string>(type: "character varying(60)", maxLength: 60, nullable: false),
NominationStartsAt = table.Column<DateOnly>(type: "date", nullable: false),
NominationEndsAt = table.Column<DateOnly>(type: "date", nullable: false),
VotingStartsAt = table.Column<DateOnly>(type: "date", nullable: false),
VotingEndsAt = table.Column<DateOnly>(type: "date", nullable: false),
ReviewStartsAt = table.Column<DateOnly>(type: "date", nullable: false),
ReviewEndsAt = table.Column<DateOnly>(type: "date", nullable: false),
ShowDate = table.Column<DateOnly>(type: "date", nullable: false),
ShowStartsAt = table.Column<TimeOnly>(type: "time without time zone", nullable: false),
WinnersPublishedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
WinnersPublishedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
SubcategoryTemplatesJson = table.Column<string>(type: "text", nullable: false, defaultValue: "[]"),
WorkflowRulesJson = table.Column<string>(type: "text", nullable: false, defaultValue: "[]")
},
constraints: table =>
{
table.PrimaryKey("PK_Seasons", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SiteSettings",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
HostDisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
HostTagline = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
NewsletterUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
ShareXUrl = table.Column<string>(type: "text", nullable: false),
ShareDiscordUrl = table.Column<string>(type: "text", nullable: false),
PrivacyEmail = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
PrivacyPolicyContent = table.Column<string>(type: "text", nullable: false),
PrivacyPolicyUpdatedBy = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
PrivacyPolicyUpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
ImprintUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
ImprintContent = table.Column<string>(type: "text", nullable: false),
ContactUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
ContactContent = table.Column<string>(type: "text", nullable: false),
SponsorsUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
SponsorsContent = table.Column<string>(type: "text", nullable: false),
ShowactsUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
ShowactsContent = table.Column<string>(type: "text", nullable: false, defaultValue: ""),
StreamBannerEyebrow = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
StreamBannerTitle = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
StreamBannerText = table.Column<string>(type: "text", nullable: false),
StreamBannerLiveButtonLabel = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
StreamBannerLiveButtonUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
StreamBannerLockedButtonLabel = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
StreamBannerUseCompletedContent = table.Column<bool>(type: "boolean", nullable: false),
StreamBannerCompletedEyebrow = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
StreamBannerCompletedTitle = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
StreamBannerCompletedText = table.Column<string>(type: "text", nullable: false),
StreamBannerCompletedButtonLabel = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
StreamBannerCompletedButtonUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
AwardsSectionTitle = table.Column<string>(type: "text", nullable: false),
AwardsSectionDescription = table.Column<string>(type: "text", nullable: false),
SubcategoriesSectionTitle = table.Column<string>(type: "text", nullable: false),
SubcategoriesSectionDescription = table.Column<string>(type: "text", nullable: false),
SocialLinksJson = table.Column<string>(type: "text", nullable: false),
FaqJson = table.Column<string>(type: "text", nullable: false),
RiskRulesJson = table.Column<string>(type: "text", nullable: false),
WorkflowRulesJson = table.Column<string>(type: "text", nullable: false, defaultValue: "[]"),
TrackingRulesJson = table.Column<string>(type: "text", nullable: false, defaultValue: "[]"),
ViewerStatsProviderBaseUrl = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
TrackingReviewNotes = table.Column<string>(type: "text", nullable: false),
NominationLinkBlacklistJson = table.Column<string>(type: "text", nullable: false, defaultValue: "[]"),
ClipSubmissionsEnabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
ClipReviewEnabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
ClipAdminMenuVisible = table.Column<bool>(type: "boolean", nullable: false),
ClipSubmissionDisabledMessage = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: false),
ShowactApplicationsEnabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
ShowactApplicationStartsAt = table.Column<DateOnly>(type: "date", nullable: true),
ShowactApplicationEndsAt = table.Column<DateOnly>(type: "date", nullable: true),
ShowactApplicationDisabledMessage = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: false),
ShowactFormSchemaJson = table.Column<string>(type: "text", nullable: false),
SponsorsVisible = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
DemoLoginManagedByDatabase = table.Column<bool>(type: "boolean", nullable: false),
DemoLoginEnabled = table.Column<bool>(type: "boolean", nullable: false),
DemoLoginEmail = table.Column<string>(type: "character varying(180)", maxLength: 180, nullable: false),
DemoLoginPasswordHash = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
DemoLoginPasswordSalt = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
DemoLoginTwitchUserId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
DemoLoginDisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
TwitchAuthManagedByDatabase = table.Column<bool>(type: "boolean", nullable: false),
TwitchClientId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
TwitchClientSecret = table.Column<string>(type: "character varying(180)", maxLength: 180, nullable: false),
TwitchRedirectUri = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
TwitchScope = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
SessionIdleTimeoutHours = table.Column<int>(type: "integer", nullable: false, defaultValue: 3),
MaintenanceModeEnabled = table.Column<bool>(type: "boolean", nullable: false),
MaintenanceTitle = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
MaintenanceMessage = table.Column<string>(type: "character varying(600)", maxLength: 600, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SiteSettings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "StreamerIdentities",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Platform = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
Login = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
NormalizedKey = table.Column<string>(type: "character varying(180)", maxLength: 180, nullable: false),
DisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
ProfileUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
LastResolvedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_StreamerIdentities", x => x.Id);
});
migrationBuilder.CreateTable(
name: "TeamMembers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Login = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
DisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Role = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
PasswordHash = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
PasswordSalt = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
BoundTwitchUserId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
BoundTwitchDisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
MustChangePassword = table.Column<bool>(type: "boolean", nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false),
CreatedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
UpdatedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LastLoginAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
TwitchBoundAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
PasswordResetAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_TeamMembers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "TeamRolePermissions",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Role = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
PermissionsJson = table.Column<string>(type: "text", nullable: false),
UpdatedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TeamRolePermissions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "UserSessions",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
SessionToken = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
TwitchUserId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
DisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Role = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
CreatedFromIp = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
UserAgent = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
LastSeenAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserSessions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
GroupName = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
Name = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Slug = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
SortOrder = table.Column<int>(type: "integer", nullable: false),
MaxNomineesPerUser = table.Column<int>(type: "integer", nullable: false),
ViewerRangeMin = table.Column<int>(type: "integer", nullable: true),
ViewerRangeMax = table.Column<int>(type: "integer", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.Id);
table.ForeignKey(
name: "FK_Categories_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "RiskFlags",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: true),
TwitchUserId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
Source = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
Type = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
Severity = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Summary = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: false),
CreatedFromIp = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
UserAgent = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
MetadataJson = table.Column<string>(type: "text", nullable: false),
ReviewNote = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
ReviewedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
ReviewedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RiskFlags", x => x.Id);
table.ForeignKey(
name: "FK_RiskFlags_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "ShowactApplications",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
ArtistName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
ContactEmail = table.Column<string>(type: "character varying(180)", maxLength: 180, nullable: false),
ContactDiscord = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
PlatformUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
PerformanceType = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: false),
TechnicalNotes = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: false),
ReferenceUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
FieldResponsesJson = table.Column<string>(type: "text", nullable: false),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
ReviewNote = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
ReviewedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
CreatedFromIp = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
UserAgent = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
ReviewedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ShowactApplications", x => x.Id);
table.ForeignKey(
name: "FK_ShowactApplications_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Sponsors",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
Name = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
WebsiteUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
LogoUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
Tier = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
SortOrder = table.Column<int>(type: "integer", nullable: false),
IsVisible = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sponsors", x => x.Id);
table.ForeignKey(
name: "FK_Sponsors_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "VoteBallots",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
SubmittedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Status = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
SubmittedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_VoteBallots", x => x.Id);
table.ForeignKey(
name: "FK_VoteBallots_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Candidates",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
CategoryId = table.Column<int>(type: "integer", nullable: false),
StreamerIdentityId = table.Column<int>(type: "integer", nullable: true),
DisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
ChannelSlug = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Platform = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
NominationTally = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
AcceptanceStatus = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false, defaultValue: "open"),
AcceptanceNote = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
ClipCompilationUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
ClipCompilationTitle = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
ClipCompilationPlatform = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: true),
ClipEmbedStatus = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false, defaultValue: "unchecked")
},
constraints: table =>
{
table.PrimaryKey("PK_Candidates", x => x.Id);
table.ForeignKey(
name: "FK_Candidates_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Candidates_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Candidates_StreamerIdentities_StreamerIdentityId",
column: x => x.StreamerIdentityId,
principalTable: "StreamerIdentities",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "ClipSubmissions",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
CategoryId = table.Column<int>(type: "integer", nullable: true),
CandidateId = table.Column<int>(type: "integer", nullable: true),
SubmittedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
ClipUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Creator = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Platform = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
ReviewNote = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
ReviewedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
CreatedFromIp = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
ReviewedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ClipSubmissions", x => x.Id);
table.ForeignKey(
name: "FK_ClipSubmissions_Candidates_CandidateId",
column: x => x.CandidateId,
principalTable: "Candidates",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_ClipSubmissions_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Nominations",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
CategoryId = table.Column<int>(type: "integer", nullable: true),
CategoryGroupName = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false, defaultValue: ""),
SubmittedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
CandidateId = table.Column<int>(type: "integer", nullable: true),
StreamerIdentityId = table.Column<int>(type: "integer", nullable: true),
SuggestedCategoryId = table.Column<int>(type: "integer", nullable: true),
CandidateText = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
StreamUrl = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: true),
ResolvedChannel = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
ResolvedPlatform = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: true),
AvgViewers = table.Column<int>(type: "integer", nullable: true),
HoursStreamed = table.Column<int>(type: "integer", nullable: true),
HoursWatched = table.Column<int>(type: "integer", nullable: true),
PeakViewers = table.Column<int>(type: "integer", nullable: true),
FollowersGained = table.Column<int>(type: "integer", nullable: true),
TrackerStatus = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false, defaultValue: "pending"),
TrackerCheckedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
TrackingReviewStatus = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false, defaultValue: "clear"),
TrackingFlagsJson = table.Column<string>(type: "text", nullable: false, defaultValue: "[]"),
TrackingReviewNote = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
TrackingReviewedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
TrackingReviewedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
ReviewNote = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
ReviewedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
ReviewedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Nominations", x => x.Id);
table.ForeignKey(
name: "FK_Nominations_Candidates_CandidateId",
column: x => x.CandidateId,
principalTable: "Candidates",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Nominations_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Nominations_Categories_SuggestedCategoryId",
column: x => x.SuggestedCategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Nominations_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Nominations_StreamerIdentities_StreamerIdentityId",
column: x => x.StreamerIdentityId,
principalTable: "StreamerIdentities",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "Results",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SeasonId = table.Column<int>(type: "integer", nullable: false),
CategoryId = table.Column<int>(type: "integer", nullable: false),
CandidateId = table.Column<int>(type: "integer", nullable: false),
CategoryName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Results", x => x.Id);
table.ForeignKey(
name: "FK_Results_Candidates_CandidateId",
column: x => x.CandidateId,
principalTable: "Candidates",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Results_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Results_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "VoteEntries",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
BallotId = table.Column<int>(type: "integer", nullable: false),
CategoryId = table.Column<int>(type: "integer", nullable: false),
CandidateId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_VoteEntries", x => x.Id);
table.ForeignKey(
name: "FK_VoteEntries_Candidates_CandidateId",
column: x => x.CandidateId,
principalTable: "Candidates",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_VoteEntries_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_VoteEntries_VoteBallots_BallotId",
column: x => x.BallotId,
principalTable: "VoteBallots",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Candidates_CategoryId",
table: "Candidates",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Candidates_SeasonId",
table: "Candidates",
column: "SeasonId");
migrationBuilder.CreateIndex(
name: "IX_Candidates_StreamerIdentityId",
table: "Candidates",
column: "StreamerIdentityId");
migrationBuilder.CreateIndex(
name: "IX_Categories_SeasonId_Slug",
table: "Categories",
columns: new[] { "SeasonId", "Slug" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClipSubmissions_CandidateId",
table: "ClipSubmissions",
column: "CandidateId");
migrationBuilder.CreateIndex(
name: "IX_ClipSubmissions_SeasonId_Status",
table: "ClipSubmissions",
columns: new[] { "SeasonId", "Status" });
migrationBuilder.CreateIndex(
name: "IX_Nominations_CandidateId",
table: "Nominations",
column: "CandidateId");
migrationBuilder.CreateIndex(
name: "IX_Nominations_CategoryId",
table: "Nominations",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Nominations_SeasonId_CategoryGroupName_Status",
table: "Nominations",
columns: new[] { "SeasonId", "CategoryGroupName", "Status" });
migrationBuilder.CreateIndex(
name: "IX_Nominations_SeasonId_Status",
table: "Nominations",
columns: new[] { "SeasonId", "Status" });
migrationBuilder.CreateIndex(
name: "IX_Nominations_SeasonId_StreamerIdentityId_CategoryGroupName",
table: "Nominations",
columns: new[] { "SeasonId", "StreamerIdentityId", "CategoryGroupName" });
migrationBuilder.CreateIndex(
name: "IX_Nominations_StreamerIdentityId",
table: "Nominations",
column: "StreamerIdentityId");
migrationBuilder.CreateIndex(
name: "IX_Nominations_SuggestedCategoryId",
table: "Nominations",
column: "SuggestedCategoryId");
migrationBuilder.CreateIndex(
name: "IX_Results_CandidateId",
table: "Results",
column: "CandidateId");
migrationBuilder.CreateIndex(
name: "IX_Results_CategoryId",
table: "Results",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Results_SeasonId_CategoryId",
table: "Results",
columns: new[] { "SeasonId", "CategoryId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_RiskFlags_SeasonId",
table: "RiskFlags",
column: "SeasonId");
migrationBuilder.CreateIndex(
name: "IX_Seasons_Year",
table: "Seasons",
column: "Year",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ShowactApplications_SeasonId_Status",
table: "ShowactApplications",
columns: new[] { "SeasonId", "Status" });
migrationBuilder.CreateIndex(
name: "IX_Sponsors_SeasonId_IsVisible_SortOrder",
table: "Sponsors",
columns: new[] { "SeasonId", "IsVisible", "SortOrder" });
migrationBuilder.CreateIndex(
name: "IX_StreamerIdentities_NormalizedKey",
table: "StreamerIdentities",
column: "NormalizedKey",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_TeamMembers_BoundTwitchUserId",
table: "TeamMembers",
column: "BoundTwitchUserId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_TeamMembers_Login",
table: "TeamMembers",
column: "Login",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_TeamRolePermissions_Role",
table: "TeamRolePermissions",
column: "Role",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserSessions_SessionToken",
table: "UserSessions",
column: "SessionToken",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_VoteBallots_SeasonId_SubmittedByTwitchId",
table: "VoteBallots",
columns: new[] { "SeasonId", "SubmittedByTwitchId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_VoteEntries_BallotId",
table: "VoteEntries",
column: "BallotId");
migrationBuilder.CreateIndex(
name: "IX_VoteEntries_CandidateId",
table: "VoteEntries",
column: "CandidateId");
migrationBuilder.CreateIndex(
name: "IX_VoteEntries_CategoryId",
table: "VoteEntries",
column: "CategoryId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AdminAuditEntries");
migrationBuilder.DropTable(
name: "ClipSubmissions");
migrationBuilder.DropTable(
name: "Nominations");
migrationBuilder.DropTable(
name: "Results");
migrationBuilder.DropTable(
name: "RiskFlags");
migrationBuilder.DropTable(
name: "ShowactApplications");
migrationBuilder.DropTable(
name: "SiteSettings");
migrationBuilder.DropTable(
name: "Sponsors");
migrationBuilder.DropTable(
name: "TeamMembers");
migrationBuilder.DropTable(
name: "TeamRolePermissions");
migrationBuilder.DropTable(
name: "UserSessions");
migrationBuilder.DropTable(
name: "VoteEntries");
migrationBuilder.DropTable(
name: "Candidates");
migrationBuilder.DropTable(
name: "VoteBallots");
migrationBuilder.DropTable(
name: "Categories");
migrationBuilder.DropTable(
name: "StreamerIdentities");
migrationBuilder.DropTable(
name: "Seasons");
}
}
}
@@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Backend.Migrations
{
[DbContext(typeof(AwardsDbContext))]
[Migration("20260629142009_InitialCleanBaseline")]
partial class InitialCleanBaseline
[Migration("20260629142100_AddLandingContentSections")]
partial class AddLandingContentSections
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddLandingContentSections : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerEyebrow" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerTitle" character varying(160) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerText" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerLiveButtonLabel" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerLiveButtonUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerLockedButtonLabel" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerUseCompletedContent" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerCompletedEyebrow" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerCompletedTitle" character varying(160) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerCompletedText" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerCompletedButtonLabel" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerCompletedButtonUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "AwardsSectionTitle" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "AwardsSectionDescription" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "SubcategoriesSectionTitle" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "SubcategoriesSectionDescription" text NOT NULL DEFAULT '';
"""
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "SubcategoriesSectionDescription";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "SubcategoriesSectionTitle";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "AwardsSectionDescription";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "AwardsSectionTitle";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerCompletedButtonUrl";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerCompletedButtonLabel";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerCompletedText";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerCompletedTitle";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerCompletedEyebrow";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerUseCompletedContent";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerLockedButtonLabel";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerLiveButtonUrl";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerLiveButtonLabel";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerText";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerTitle";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "StreamBannerEyebrow";
"""
);
}
}
}
@@ -0,0 +1,91 @@
using Backend.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations;
[DbContext(typeof(AwardsDbContext))]
[Migration("20260629142200_EnsureSiteSettingsSeedColumns")]
public partial class EnsureSiteSettingsSeedColumns : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "HostDisplayName" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "HostTagline" character varying(160) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "NewsletterUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ShareXUrl" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ShareDiscordUrl" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "PrivacyEmail" character varying(160) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "PrivacyPolicyContent" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "PrivacyPolicyUpdatedBy" character varying(120);
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "PrivacyPolicyUpdatedAt" timestamp with time zone;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ImprintUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ImprintContent" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ContactUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ContactContent" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "SponsorsUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "SponsorsContent" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ShowactsUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ShowactsContent" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerEyebrow" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerTitle" character varying(160) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerText" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerLiveButtonLabel" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerLiveButtonUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerLockedButtonLabel" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerUseCompletedContent" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerCompletedEyebrow" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerCompletedTitle" character varying(160) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerCompletedText" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerCompletedButtonLabel" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "StreamBannerCompletedButtonUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "AwardsSectionTitle" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "AwardsSectionDescription" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "SubcategoriesSectionTitle" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "SubcategoriesSectionDescription" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "SocialLinksJson" text NOT NULL DEFAULT '[]';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "FaqJson" text NOT NULL DEFAULT '[]';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "RiskRulesJson" text NOT NULL DEFAULT '[]';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "WorkflowRulesJson" text NOT NULL DEFAULT '[]';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "TrackingRulesJson" text NOT NULL DEFAULT '[]';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ViewerStatsProviderBaseUrl" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "TrackingReviewNotes" text NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "NominationLinkBlacklistJson" text NOT NULL DEFAULT '[]';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ClipSubmissionsEnabled" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ClipReviewEnabled" boolean NOT NULL DEFAULT TRUE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ClipAdminMenuVisible" boolean NOT NULL DEFAULT TRUE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ClipSubmissionDisabledMessage" character varying(240) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ShowactApplicationsEnabled" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ShowactApplicationStartsAt" date;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ShowactApplicationEndsAt" date;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ShowactApplicationDisabledMessage" character varying(240) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "ShowactFormSchemaJson" text NOT NULL DEFAULT '[]';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "SponsorsVisible" boolean NOT NULL DEFAULT TRUE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "DemoLoginManagedByDatabase" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "DemoLoginEnabled" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "DemoLoginEmail" character varying(180) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "DemoLoginPasswordHash" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "DemoLoginPasswordSalt" character varying(80) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "DemoLoginTwitchUserId" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "DemoLoginDisplayName" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "TwitchAuthManagedByDatabase" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "TwitchClientId" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "TwitchClientSecret" character varying(180) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "TwitchRedirectUri" character varying(400) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "TwitchScope" character varying(300) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "SessionIdleTimeoutHours" integer NOT NULL DEFAULT 3;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "MaintenanceModeEnabled" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "MaintenanceTitle" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "MaintenanceMessage" character varying(600) NOT NULL DEFAULT '';
"""
);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
@@ -0,0 +1,73 @@
using Backend.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations;
[DbContext(typeof(AwardsDbContext))]
[Migration("20260629142746_EnsureDemoSeedSchema")]
public partial class EnsureDemoSeedSchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "Seasons" ADD COLUMN IF NOT EXISTS "IsDemo" boolean NOT NULL DEFAULT FALSE;
ALTER TABLE "Seasons" ADD COLUMN IF NOT EXISTS "ShowStartsAt" time without time zone NOT NULL DEFAULT TIME '20:00';
ALTER TABLE "Seasons" ADD COLUMN IF NOT EXISTS "WinnersPublishedAt" timestamp with time zone;
ALTER TABLE "Seasons" ADD COLUMN IF NOT EXISTS "WinnersPublishedByTwitchId" character varying(120);
ALTER TABLE "Seasons" ADD COLUMN IF NOT EXISTS "SubcategoryTemplatesJson" text NOT NULL DEFAULT '[]';
ALTER TABLE "Seasons" ADD COLUMN IF NOT EXISTS "WorkflowRulesJson" text NOT NULL DEFAULT '[]';
ALTER TABLE "Categories" ADD COLUMN IF NOT EXISTS "ViewerRangeMin" integer;
ALTER TABLE "Categories" ADD COLUMN IF NOT EXISTS "ViewerRangeMax" integer;
ALTER TABLE "Candidates" ADD COLUMN IF NOT EXISTS "StreamerIdentityId" integer;
ALTER TABLE "Candidates" ADD COLUMN IF NOT EXISTS "NominationTally" integer NOT NULL DEFAULT 0;
ALTER TABLE "Candidates" ADD COLUMN IF NOT EXISTS "AcceptanceStatus" character varying(30) NOT NULL DEFAULT 'open';
ALTER TABLE "Candidates" ADD COLUMN IF NOT EXISTS "AcceptanceNote" character varying(500);
ALTER TABLE "Candidates" ADD COLUMN IF NOT EXISTS "ClipCompilationUrl" character varying(500);
ALTER TABLE "Candidates" ADD COLUMN IF NOT EXISTS "ClipCompilationTitle" character varying(200);
ALTER TABLE "Candidates" ADD COLUMN IF NOT EXISTS "ClipCompilationPlatform" character varying(40);
ALTER TABLE "Candidates" ADD COLUMN IF NOT EXISTS "ClipEmbedStatus" character varying(30) NOT NULL DEFAULT 'unchecked';
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "CategoryGroupName" character varying(80) NOT NULL DEFAULT '';
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "StreamerIdentityId" integer;
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "SuggestedCategoryId" integer;
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "ResolvedChannel" character varying(120);
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "ResolvedPlatform" character varying(40);
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "AvgViewers" integer;
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "HoursStreamed" integer;
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "HoursWatched" integer;
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "PeakViewers" integer;
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "FollowersGained" integer;
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "TrackerStatus" character varying(40) NOT NULL DEFAULT 'pending';
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "TrackerCheckedAt" timestamp with time zone;
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "TrackingReviewStatus" character varying(30) NOT NULL DEFAULT 'clear';
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "TrackingFlagsJson" text NOT NULL DEFAULT '[]';
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "TrackingReviewNote" character varying(1000);
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "TrackingReviewedByTwitchId" character varying(120);
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "TrackingReviewedAt" timestamp with time zone;
DO $vtsa_optional_demo_seed_schema$
BEGIN
IF to_regclass('"ClipSubmissions"') IS NOT NULL THEN
ALTER TABLE "ClipSubmissions" ADD COLUMN IF NOT EXISTS "CategoryId" integer;
ALTER TABLE "ClipSubmissions" ADD COLUMN IF NOT EXISTS "CandidateId" integer;
END IF;
IF to_regclass('"ShowactApplications"') IS NOT NULL THEN
ALTER TABLE "ShowactApplications" ADD COLUMN IF NOT EXISTS "FieldResponsesJson" text NOT NULL DEFAULT '{}';
END IF;
END;
$vtsa_optional_demo_seed_schema$;
"""
);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
@@ -10,6 +10,13 @@ public partial class SeedRealisticDemoScenario : Migration
{
migrationBuilder.Sql(
"""
DO $vtsa_seed$
BEGIN
IF EXISTS (SELECT 1 FROM "Seasons" WHERE "Year" IN (2025, 2026)) THEN
RAISE NOTICE 'Existing seasons detected; skipping realistic demo seed.';
RETURN;
END IF;
INSERT INTO "Seasons" (
"Id", "Year", "Name", "IsDemo", "IsCurrent", "IsCommunityOnly", "CurrentPhase",
"NominationStartsAt", "NominationEndsAt", "VotingStartsAt", "VotingEndsAt",
@@ -240,6 +247,8 @@ public partial class SeedRealisticDemoScenario : Migration
SELECT setval(pg_get_serial_sequence('"ShowactApplications"', 'Id'), COALESCE((SELECT MAX("Id") FROM "ShowactApplications"), 1));
SELECT setval(pg_get_serial_sequence('"Sponsors"', 'Id'), COALESCE((SELECT MAX("Id") FROM "Sponsors"), 1));
SELECT setval(pg_get_serial_sequence('"RiskFlags"', 'Id'), COALESCE((SELECT MAX("Id") FROM "RiskFlags"), 1));
END;
$vtsa_seed$;
"""
);
}
@@ -10,6 +10,18 @@ public partial class AdjustDemoSubcategoriesAndArchive : Migration
{
migrationBuilder.Sql(
"""
DO $vtsa_adjust$
BEGIN
IF EXISTS (
SELECT 1
FROM "Seasons"
WHERE "Year" IN (2024, 2025, 2026)
AND "Id" NOT IN (999, 1000, 1001)
) THEN
RAISE NOTICE 'Existing non-demo season years detected; skipping demo archive reseed.';
RETURN;
END IF;
DELETE FROM "RiskFlags" WHERE "SeasonId" IN (999, 1000, 1001);
DELETE FROM "Seasons" WHERE "Id" IN (999, 1000, 1001) AND "IsDemo" = TRUE;
DELETE FROM "StreamerIdentities" WHERE "Id" BETWEEN 3001 AND 3020;
@@ -275,6 +287,8 @@ public partial class AdjustDemoSubcategoriesAndArchive : Migration
SELECT setval(pg_get_serial_sequence('"ShowactApplications"', 'Id'), COALESCE((SELECT MAX("Id") FROM "ShowactApplications"), 1));
SELECT setval(pg_get_serial_sequence('"Sponsors"', 'Id'), COALESCE((SELECT MAX("Id") FROM "Sponsors"), 1));
SELECT setval(pg_get_serial_sequence('"RiskFlags"', 'Id'), COALESCE((SELECT MAX("Id") FROM "RiskFlags"), 1));
END;
$vtsa_adjust$;
"""
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,48 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class AddArchivedWinners : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ArchivedWinners",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Year = table.Column<int>(type: "integer", nullable: false),
Category = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Subcategory = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
WinnerName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
WinnerUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ArchivedWinners", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ArchivedWinners_Year_Category_Subcategory",
table: "ArchivedWinners",
columns: new[] { "Year", "Category", "Subcategory" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ArchivedWinners");
}
}
}
@@ -0,0 +1,306 @@
using Backend.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations;
[DbContext(typeof(AwardsDbContext))]
[Migration("20260629183000_SeedLiveDemoAwardData")]
public partial class SeedLiveDemoAwardData : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
DO $vtsa_live_demo$
DECLARE
demo_year integer := 2030;
demo_season_id integer;
should_make_current boolean;
subcategory_templates text := '[{"name":"Hidden Star","slug":"hidden-star","sortOrder":1,"viewerRangeMin":1,"viewerRangeMax":20},{"name":"Rising Star","slug":"rising-star","sortOrder":2,"viewerRangeMin":21,"viewerRangeMax":60},{"name":"Shining Star","slug":"shining-star","sortOrder":3,"viewerRangeMin":61,"viewerRangeMax":null}]';
BEGIN
SELECT NOT EXISTS (SELECT 1 FROM "Seasons" WHERE "IsCurrent" = TRUE)
INTO should_make_current;
INSERT INTO "Seasons" (
"Year",
"Name",
"IsDemo",
"IsCurrent",
"IsCommunityOnly",
"CurrentPhase",
"NominationStartsAt",
"NominationEndsAt",
"VotingStartsAt",
"VotingEndsAt",
"ReviewStartsAt",
"ReviewEndsAt",
"ShowDate",
"ShowStartsAt",
"WinnersPublishedAt",
"WinnersPublishedByTwitchId",
"SubcategoryTemplatesJson",
"WorkflowRulesJson"
)
VALUES (
demo_year,
'VTuber Star Awards Demo Season',
TRUE,
should_make_current,
FALSE,
'Voting',
DATE '2030-05-01',
DATE '2030-05-31',
DATE '2030-06-01',
DATE '2030-06-30',
DATE '2030-07-01',
DATE '2030-07-10',
DATE '2030-07-20',
TIME '20:00',
NULL,
NULL,
subcategory_templates,
'[]'
)
ON CONFLICT ("Year") DO UPDATE
SET
"Name" = EXCLUDED."Name",
"IsDemo" = TRUE,
"IsCurrent" = CASE
WHEN should_make_current THEN TRUE
ELSE "Seasons"."IsCurrent"
END,
"IsCommunityOnly" = FALSE,
"CurrentPhase" = 'Voting',
"NominationStartsAt" = DATE '2030-05-01',
"NominationEndsAt" = DATE '2030-05-31',
"VotingStartsAt" = DATE '2030-06-01',
"VotingEndsAt" = DATE '2030-06-30',
"ReviewStartsAt" = DATE '2030-07-01',
"ReviewEndsAt" = DATE '2030-07-10',
"ShowDate" = DATE '2030-07-20',
"ShowStartsAt" = TIME '20:00',
"SubcategoryTemplatesJson" = subcategory_templates
RETURNING "Id" INTO demo_season_id;
IF should_make_current THEN
UPDATE "Seasons"
SET "IsCurrent" = FALSE
WHERE "Id" <> demo_season_id;
UPDATE "Seasons"
SET "IsCurrent" = TRUE
WHERE "Id" = demo_season_id;
END IF;
WITH award_groups(ord, group_name, group_slug, description, name_prefix) AS (
VALUES
(1, 'Creator des Jahres', 'creator-des-jahres', 'Allround-Creator mit starker Season, klarer Community-Praesenz und wiedererkennbarer Identitaet.', 'Astra'),
(2, 'Newcomer', 'newcomer', 'Neue Stimmen, Debuets und Creator, die in dieser Season besonders sichtbar geworden sind.', 'Nova'),
(3, 'Model und Design', 'model-design', 'Live2D, 3D, Outfit, Rigging und visuelle Praesentation im Stream.', 'Velvet'),
(4, 'Gesang und Musik', 'gesang-musik', 'Covers, Originale, Live-Gesang, Musikproduktion und Buehnenmomente.', 'Melo'),
(5, 'Gaming', 'gaming', 'Gameplay, Skill, Chaos, Speedruns und starke Gaming-Unterhaltung.', 'Pixel'),
(6, 'Variety und Entertainment', 'variety-entertainment', 'Talk, Comedy, Watchalongs, Challenges und kreative Streamformate.', 'Taro'),
(7, 'Community', 'community', 'Creator, die ihre Community sichtbar verbinden, einladen und aktiv einbeziehen.', 'Lumi'),
(8, 'Collab und Duo', 'collab-duo', 'Gemeinsame Streams, Duo-Dynamik und Projekte, die zusammen staerker wirken.', 'Orbit')
),
tiers(ord, tier_name, tier_slug, min_viewers, max_viewers) AS (
VALUES
(1, 'Hidden Star', 'hidden-star', 1, 20),
(2, 'Rising Star', 'rising-star', 21, 60),
(3, 'Shining Star', 'shining-star', 61, NULL)
)
INSERT INTO "Categories" (
"SeasonId",
"GroupName",
"Name",
"Slug",
"Description",
"SortOrder",
"MaxNomineesPerUser",
"ViewerRangeMin",
"ViewerRangeMax"
)
SELECT
demo_season_id,
award_groups.group_name,
tiers.tier_name,
award_groups.group_slug || '-' || tiers.tier_slug,
award_groups.description,
(award_groups.ord * 100) + (tiers.ord * 10),
3,
tiers.min_viewers,
tiers.max_viewers
FROM award_groups
CROSS JOIN tiers
ON CONFLICT ("SeasonId", "Slug") DO UPDATE
SET
"GroupName" = EXCLUDED."GroupName",
"Name" = EXCLUDED."Name",
"Description" = EXCLUDED."Description",
"SortOrder" = EXCLUDED."SortOrder",
"MaxNomineesPerUser" = EXCLUDED."MaxNomineesPerUser",
"ViewerRangeMin" = EXCLUDED."ViewerRangeMin",
"ViewerRangeMax" = EXCLUDED."ViewerRangeMax";
WITH award_groups(ord, group_name, group_slug, name_prefix) AS (
VALUES
(1, 'Creator des Jahres', 'creator-des-jahres', 'Astra'),
(2, 'Newcomer', 'newcomer', 'Nova'),
(3, 'Model und Design', 'model-design', 'Velvet'),
(4, 'Gesang und Musik', 'gesang-musik', 'Melo'),
(5, 'Gaming', 'gaming', 'Pixel'),
(6, 'Variety und Entertainment', 'variety-entertainment', 'Taro'),
(7, 'Community', 'community', 'Lumi'),
(8, 'Collab und Duo', 'collab-duo', 'Orbit')
),
tiers(ord, tier_name, tier_slug) AS (
VALUES
(1, 'Hidden Star', 'hidden-star'),
(2, 'Rising Star', 'rising-star'),
(3, 'Shining Star', 'shining-star')
),
slots(ord, name_suffix, platform) AS (
VALUES
(1, 'Aster', 'Twitch'),
(2, 'Vega', 'YouTube')
),
candidate_rows AS (
SELECT
categories."Id" AS category_id,
award_groups.ord AS group_ord,
tiers.ord AS tier_ord,
slots.ord AS slot_ord,
award_groups.name_prefix || ' ' || slots.name_suffix || ' (' || tiers.tier_name || ')' AS display_name,
regexp_replace(
lower('vtsa_demo_' || award_groups.name_prefix || '_' || tiers.tier_slug || '_' || slots.name_suffix),
'[^a-z0-9]+',
'_',
'g'
) AS channel_slug,
slots.platform
FROM award_groups
CROSS JOIN tiers
CROSS JOIN slots
INNER JOIN "Categories" categories
ON categories."SeasonId" = demo_season_id
AND categories."Slug" = award_groups.group_slug || '-' || tiers.tier_slug
)
INSERT INTO "Candidates" (
"SeasonId",
"CategoryId",
"StreamerIdentityId",
"DisplayName",
"ChannelSlug",
"Platform",
"NominationTally",
"AcceptanceStatus",
"AcceptanceNote",
"ClipCompilationUrl",
"ClipCompilationTitle",
"ClipCompilationPlatform",
"ClipEmbedStatus"
)
SELECT
demo_season_id,
candidate_rows.category_id,
NULL,
candidate_rows.display_name,
candidate_rows.channel_slug,
candidate_rows.platform,
4 + (candidate_rows.group_ord * 2) + candidate_rows.tier_ord + candidate_rows.slot_ord,
'approved',
'Demo-Kandidat fuer Live-Testdaten.',
CASE
WHEN candidate_rows.slot_ord = 1 THEN 'https://youtu.be/demo-' || candidate_rows.channel_slug
ELSE NULL
END,
CASE
WHEN candidate_rows.slot_ord = 1 THEN candidate_rows.display_name || ' Highlight Reel'
ELSE NULL
END,
CASE
WHEN candidate_rows.slot_ord = 1 THEN 'YouTube'
ELSE NULL
END,
CASE
WHEN candidate_rows.slot_ord = 1 THEN 'available'
ELSE 'unchecked'
END
FROM candidate_rows
WHERE NOT EXISTS (
SELECT 1
FROM "Candidates" existing
WHERE existing."SeasonId" = demo_season_id
AND lower(existing."ChannelSlug") = lower(candidate_rows.channel_slug)
AND existing."CategoryId" = candidate_rows.category_id
);
PERFORM setval(pg_get_serial_sequence('"Seasons"', 'Id'), COALESCE((SELECT MAX("Id") FROM "Seasons"), 1));
PERFORM setval(pg_get_serial_sequence('"Categories"', 'Id'), COALESCE((SELECT MAX("Id") FROM "Categories"), 1));
PERFORM setval(pg_get_serial_sequence('"Candidates"', 'Id'), COALESCE((SELECT MAX("Id") FROM "Candidates"), 1));
END;
$vtsa_live_demo$;
"""
);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
DO $vtsa_live_demo_down$
DECLARE
demo_season_id integer;
BEGIN
SELECT "Id"
INTO demo_season_id
FROM "Seasons"
WHERE "Year" = 2030
AND "IsDemo" = TRUE
AND "Name" = 'VTuber Star Awards Demo Season';
IF demo_season_id IS NULL THEN
RETURN;
END IF;
DELETE FROM "Candidates"
WHERE "SeasonId" = demo_season_id
AND "ChannelSlug" LIKE 'vtsa_demo_%';
DELETE FROM "Categories"
WHERE "SeasonId" = demo_season_id
AND "Slug" IN (
'creator-des-jahres-hidden-star',
'creator-des-jahres-rising-star',
'creator-des-jahres-shining-star',
'newcomer-hidden-star',
'newcomer-rising-star',
'newcomer-shining-star',
'model-design-hidden-star',
'model-design-rising-star',
'model-design-shining-star',
'gesang-musik-hidden-star',
'gesang-musik-rising-star',
'gesang-musik-shining-star',
'gaming-hidden-star',
'gaming-rising-star',
'gaming-shining-star',
'variety-entertainment-hidden-star',
'variety-entertainment-rising-star',
'variety-entertainment-shining-star',
'community-hidden-star',
'community-rising-star',
'community-shining-star',
'collab-duo-hidden-star',
'collab-duo-rising-star',
'collab-duo-shining-star'
);
END;
$vtsa_live_demo_down$;
"""
);
}
}
@@ -0,0 +1,206 @@
using Backend.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations;
[DbContext(typeof(AwardsDbContext))]
[Migration("20260629185000_SeedCurrentSeasonDemoVotingData")]
public partial class SeedCurrentSeasonDemoVotingData : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
DO $vtsa_current_demo$
DECLARE
target_season_id integer;
subcategory_templates text := '[{"name":"Hidden Star","slug":"hidden-star","sortOrder":1,"viewerRangeMin":1,"viewerRangeMax":20},{"name":"Rising Star","slug":"rising-star","sortOrder":2,"viewerRangeMin":21,"viewerRangeMax":60},{"name":"Shining Star","slug":"shining-star","sortOrder":3,"viewerRangeMin":61,"viewerRangeMax":null}]';
BEGIN
SELECT "Id"
INTO target_season_id
FROM "Seasons"
WHERE "IsCurrent" = TRUE
ORDER BY "Year" DESC
LIMIT 1;
IF target_season_id IS NULL THEN
SELECT "Id"
INTO target_season_id
FROM "Seasons"
WHERE "Year" = 2030
ORDER BY "Id" DESC
LIMIT 1;
END IF;
IF target_season_id IS NULL THEN
RETURN;
END IF;
UPDATE "Seasons"
SET "SubcategoryTemplatesJson" = subcategory_templates
WHERE "Id" = target_season_id;
WITH award_groups(ord, group_name, group_slug, description, name_prefix) AS (
VALUES
(1, 'Main Awards', 'main-awards', 'Die groessten Allround-Auszeichnungen der aktuellen Season.', 'Astra'),
(2, 'Discovery', 'discovery', 'Neue Stimmen, Debuets und Creator, die in dieser Season besonders sichtbar geworden sind.', 'Nova'),
(3, 'Creative', 'creative', 'Model, Design, Rigging, Art und visuelle Praesentation im Stream.', 'Velvet'),
(4, 'Performance', 'performance', 'Gesang, Musik, Buehnenpraesenz und besondere Live-Momente.', 'Melo'),
(5, 'Gaming', 'gaming', 'Gameplay, Skill, Chaos, Speedruns und starke Gaming-Unterhaltung.', 'Pixel'),
(6, 'Entertainment', 'entertainment', 'Talk, Comedy, Watchalongs, Challenges und kreative Streamformate.', 'Taro'),
(7, 'Community', 'community', 'Creator, die ihre Community sichtbar verbinden, einladen und aktiv einbeziehen.', 'Lumi'),
(8, 'Collab', 'collab', 'Gemeinsame Streams, Duo-Dynamik und Projekte, die zusammen staerker wirken.', 'Orbit')
),
tiers(ord, tier_name, tier_slug, min_viewers, max_viewers) AS (
VALUES
(1, 'Hidden Star', 'hidden-star', 1, 20),
(2, 'Rising Star', 'rising-star', 21, 60),
(3, 'Shining Star', 'shining-star', 61, NULL)
)
INSERT INTO "Categories" (
"SeasonId",
"GroupName",
"Name",
"Slug",
"Description",
"SortOrder",
"MaxNomineesPerUser",
"ViewerRangeMin",
"ViewerRangeMax"
)
SELECT
target_season_id,
award_groups.group_name,
tiers.tier_name,
award_groups.group_slug || '-' || tiers.tier_slug,
award_groups.description,
(award_groups.ord * 100) + (tiers.ord * 10),
3,
tiers.min_viewers,
tiers.max_viewers
FROM award_groups
CROSS JOIN tiers
ON CONFLICT ("SeasonId", "Slug") DO UPDATE
SET
"GroupName" = EXCLUDED."GroupName",
"Name" = EXCLUDED."Name",
"Description" = EXCLUDED."Description",
"SortOrder" = EXCLUDED."SortOrder",
"MaxNomineesPerUser" = EXCLUDED."MaxNomineesPerUser",
"ViewerRangeMin" = EXCLUDED."ViewerRangeMin",
"ViewerRangeMax" = EXCLUDED."ViewerRangeMax";
WITH award_groups(ord, group_name, group_slug, name_prefix) AS (
VALUES
(1, 'Main Awards', 'main-awards', 'Astra'),
(2, 'Discovery', 'discovery', 'Nova'),
(3, 'Creative', 'creative', 'Velvet'),
(4, 'Performance', 'performance', 'Melo'),
(5, 'Gaming', 'gaming', 'Pixel'),
(6, 'Entertainment', 'entertainment', 'Taro'),
(7, 'Community', 'community', 'Lumi'),
(8, 'Collab', 'collab', 'Orbit')
),
tiers(ord, tier_name, tier_slug) AS (
VALUES
(1, 'Hidden Star', 'hidden-star'),
(2, 'Rising Star', 'rising-star'),
(3, 'Shining Star', 'shining-star')
),
slots(ord, name_suffix, platform) AS (
VALUES
(1, 'Aster', 'Twitch'),
(2, 'Vega', 'YouTube')
),
candidate_rows AS (
SELECT
categories."Id" AS category_id,
award_groups.ord AS group_ord,
tiers.ord AS tier_ord,
slots.ord AS slot_ord,
award_groups.name_prefix || ' ' || slots.name_suffix || ' (' || tiers.tier_name || ')' AS display_name,
regexp_replace(
lower('vtsa_demo_current_' || award_groups.name_prefix || '_' || tiers.tier_slug || '_' || slots.name_suffix),
'[^a-z0-9]+',
'_',
'g'
) AS channel_slug,
slots.platform
FROM award_groups
CROSS JOIN tiers
CROSS JOIN slots
INNER JOIN "Categories" categories
ON categories."SeasonId" = target_season_id
AND categories."Slug" = award_groups.group_slug || '-' || tiers.tier_slug
)
INSERT INTO "Candidates" (
"SeasonId",
"CategoryId",
"StreamerIdentityId",
"DisplayName",
"ChannelSlug",
"Platform",
"NominationTally",
"AcceptanceStatus",
"AcceptanceNote",
"ClipCompilationUrl",
"ClipCompilationTitle",
"ClipCompilationPlatform",
"ClipEmbedStatus"
)
SELECT
target_season_id,
candidate_rows.category_id,
NULL,
candidate_rows.display_name,
candidate_rows.channel_slug,
candidate_rows.platform,
4 + (candidate_rows.group_ord * 2) + candidate_rows.tier_ord + candidate_rows.slot_ord,
'approved',
'Demo-Kandidat fuer den aktuellen Live-Voting-Test.',
CASE
WHEN candidate_rows.slot_ord = 1 THEN 'https://youtu.be/demo-' || candidate_rows.channel_slug
ELSE NULL
END,
CASE
WHEN candidate_rows.slot_ord = 1 THEN candidate_rows.display_name || ' Highlight Reel'
ELSE NULL
END,
CASE
WHEN candidate_rows.slot_ord = 1 THEN 'YouTube'
ELSE NULL
END,
CASE
WHEN candidate_rows.slot_ord = 1 THEN 'available'
ELSE 'unchecked'
END
FROM candidate_rows
WHERE NOT EXISTS (
SELECT 1
FROM "Candidates" existing
WHERE existing."SeasonId" = target_season_id
AND lower(existing."ChannelSlug") = lower(candidate_rows.channel_slug)
AND existing."CategoryId" = candidate_rows.category_id
);
PERFORM setval(pg_get_serial_sequence('"Categories"', 'Id'), COALESCE((SELECT MAX("Id") FROM "Categories"), 1));
PERFORM setval(pg_get_serial_sequence('"Candidates"', 'Id'), COALESCE((SELECT MAX("Id") FROM "Candidates"), 1));
END;
$vtsa_current_demo$;
"""
);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
DELETE FROM "Candidates"
WHERE "ChannelSlug" LIKE 'vtsa_demo_current_%';
"""
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,72 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class SeedDemoSponsorsAndShareLinks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
UPDATE "SiteSettings"
SET
"ShareXUrl" = CASE
WHEN COALESCE(NULLIF("ShareXUrl", ''), '') IN ('', 'https://x.com/intent/tweet')
THEN 'https://x.com/intent/tweet?text=Schaut%20euch%20die%20VTuber%20Star%20Awards%20an!&url=https%3A%2F%2Faward.noveria.net'
ELSE "ShareXUrl"
END,
"ShareDiscordUrl" = CASE
WHEN COALESCE(NULLIF("ShareDiscordUrl", ''), '') IN ('', 'https://discord.gg/')
THEN 'https://discord.gg/jayuhime'
ELSE "ShareDiscordUrl"
END
WHERE "Id" = 1;
INSERT INTO "Sponsors" (
"Id", "SeasonId", "Name", "WebsiteUrl", "LogoUrl", "Description", "Tier", "SortOrder", "IsVisible", "CreatedAt", "UpdatedAt"
)
SELECT seed."Id", seed."SeasonId", seed."Name", seed."WebsiteUrl", seed."LogoUrl", seed."Description", seed."Tier", seed."SortOrder", seed."IsVisible", seed."CreatedAt", seed."UpdatedAt"
FROM (
VALUES
(9006, 1001, 'Starfall Merch Lab', 'https://example.invalid/starfall-merch', '/demo/sponsors/chibicanvas-market.svg', 'Merch-Pakete, Sticker-Bundles und kleine Giveaways fuer Community-Aktionen rund um die Show.', 'Merch Partner', 60, TRUE, TIMESTAMPTZ '2026-06-01 10:25:00+00', NULL::timestamptz),
(9007, 1001, 'Moonframe Media', 'https://example.invalid/moonframe-media', '/demo/sponsors/prismloop-audio.svg', 'Highlight-Cuts, Social-Assets und kurze Promo-Snippets fuer Voting- und Finale-Phasen.', 'Media Partner', 70, TRUE, TIMESTAMPTZ '2026-06-01 10:30:00+00', NULL::timestamptz),
(9008, 1001, 'PixelHarbor Tools', 'https://example.invalid/pixelharbor-tools', '/demo/sponsors/cloudbeacon-hosting.svg', 'Kleine Creator-Tools fuer Landingpages, Formulare und Community-Orga im Eventbetrieb.', 'Tooling Partner', 80, TRUE, TIMESTAMPTZ '2026-06-01 10:35:00+00', NULL::timestamptz)
) AS seed("Id", "SeasonId", "Name", "WebsiteUrl", "LogoUrl", "Description", "Tier", "SortOrder", "IsVisible", "CreatedAt", "UpdatedAt")
WHERE EXISTS (SELECT 1 FROM "Seasons" WHERE "Id" = seed."SeasonId")
AND NOT EXISTS (
SELECT 1
FROM "Sponsors" existing
WHERE existing."SeasonId" = seed."SeasonId"
AND existing."Name" = seed."Name"
);
SELECT setval(pg_get_serial_sequence('"Sponsors"', 'Id'), COALESCE((SELECT MAX("Id") FROM "Sponsors"), 1));
"""
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
DELETE FROM "Sponsors"
WHERE "Id" IN (9006, 9007, 9008)
AND "SeasonId" = 1001;
UPDATE "SiteSettings"
SET
"ShareXUrl" = 'https://x.com/intent/tweet',
"ShareDiscordUrl" = 'https://discord.gg/'
WHERE "Id" = 1
AND "ShareXUrl" = 'https://x.com/intent/tweet?text=Schaut%20euch%20die%20VTuber%20Star%20Awards%20an!&url=https%3A%2F%2Faward.noveria.net'
AND "ShareDiscordUrl" = 'https://discord.gg/jayuhime';
"""
);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
/// <inheritdoc />
public partial class SeedExampleSponsorsForCurrentSeason : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
WITH target_season AS (
SELECT "Id"
FROM "Seasons"
WHERE "IsCurrent" = TRUE
ORDER BY "Year" DESC, "Id" DESC
LIMIT 1
)
INSERT INTO "Sponsors" (
"SeasonId", "Name", "WebsiteUrl", "LogoUrl", "Description", "Tier", "SortOrder", "IsVisible", "CreatedAt", "UpdatedAt"
)
SELECT
target_season."Id",
seed."Name",
seed."WebsiteUrl",
seed."LogoUrl",
seed."Description",
seed."Tier",
seed."SortOrder",
TRUE,
TIMESTAMPTZ '2026-06-29 19:22:20+00',
NULL::timestamptz
FROM target_season
CROSS JOIN (
VALUES
('HoshiForge Studio', 'https://example.invalid/hoshiforge', '/demo/sponsors/hoshiforge-studio.svg', 'Branding-, Overlay- und Debuet-Visuals fuer VTuber-Projekte und Community-Events.', 'Presenting Sponsor', 10),
('NekoPixel Energy', 'https://example.invalid/nekopixel', '/demo/sponsors/nekopixel-energy.svg', 'Community-fokussierter Drink-Partner fuer lange Showabende, Watchpartys und Creator-Collabs.', 'Gold Partner', 20),
('PrismLoop Audio', 'https://example.invalid/prismloop', '/demo/sponsors/prismloop-audio.svg', 'Audio-Tools, Intro-Packs und Stream-Sounddesign fuer Live-Shows und Highlight-Clips.', 'Gold Partner', 30),
('CloudBeacon Hosting', 'https://example.invalid/cloudbeacon', '/demo/sponsors/cloudbeacon-hosting.svg', 'Skalierbares Hosting fuer Voting, Landingpages und Event-Traffic rund um Showtage.', 'Tech Partner', 40),
('ChibiCanvas Market', 'https://example.invalid/chibicanvas', '/demo/sponsors/chibicanvas-market.svg', 'Merch-, Sticker- und Artist-Marketplace mit Fokus auf VTuber, Emotes und Fanartikel.', 'Community Partner', 50)
) AS seed("Name", "WebsiteUrl", "LogoUrl", "Description", "Tier", "SortOrder")
WHERE NOT EXISTS (
SELECT 1
FROM "Sponsors" existing
WHERE existing."SeasonId" = target_season."Id"
);
"""
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
DELETE FROM "Sponsors"
WHERE "Name" IN (
'HoshiForge Studio',
'NekoPixel Energy',
'PrismLoop Audio',
'CloudBeacon Hosting',
'ChibiCanvas Market'
);
"""
);
}
}
}
@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations;
[DbContext(typeof(Data.AwardsDbContext))]
[Migration("20260629194000_RemoveDemoSeasons")]
public partial class RemoveDemoSeasons : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
UPDATE "RiskFlags"
SET "SeasonId" = NULL
WHERE "SeasonId" IN (
SELECT "Id"
FROM "Seasons"
WHERE "IsDemo" = TRUE
);
DELETE FROM "Seasons"
WHERE "IsDemo" = TRUE;
"""
);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
@@ -0,0 +1,38 @@
using Backend.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
[DbContext(typeof(AwardsDbContext))]
[Migration("20260629203000_AddHostImageUpload")]
/// <inheritdoc />
public partial class AddHostImageUpload : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "HostImageData" bytea;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "HostImageContentType" character varying(80);
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "HostImageUpdatedAt" timestamp with time zone;
"""
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "HostImageUpdatedAt";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "HostImageContentType";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "HostImageData";
"""
);
}
}
}
@@ -0,0 +1,34 @@
using Backend.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations
{
[DbContext(typeof(AwardsDbContext))]
[Migration("20260629204500_AddHostArtistName")]
/// <inheritdoc />
public partial class AddHostArtistName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "HostArtistName" character varying(120) NOT NULL DEFAULT '';
"""
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "HostArtistName";
"""
);
}
}
}
@@ -0,0 +1,38 @@
using Backend.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Backend.Migrations;
[DbContext(typeof(AwardsDbContext))]
[Migration("20260629210000_EnsureRuntimeSchemaParity")]
public partial class EnsureRuntimeSchemaParity : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "HostImageData" bytea;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "HostImageContentType" character varying(80);
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "HostImageUpdatedAt" timestamp with time zone;
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "HostArtistName" character varying(120) NOT NULL DEFAULT '';
ALTER TABLE "Nominations" ADD COLUMN IF NOT EXISTS "StreamUrl" character varying(300);
"""
);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "Nominations" DROP COLUMN IF EXISTS "StreamUrl";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "HostArtistName";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "HostImageUpdatedAt";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "HostImageContentType";
ALTER TABLE "SiteSettings" DROP COLUMN IF EXISTS "HostImageData";
"""
);
}
}

Some files were not shown because too many files have changed in this diff Show More