Add viewer-range categories, nomination tracking, dynamic showact form, session timeout, share URLs, and workflow-per-season

Features:
- Category viewer ranges + subcategory templates (admin group modal, tree workspace)
- Nomination enrichment via TwitchTracker API (NominationEnrichmentService,
  TwitchTrackerViewerStatsProvider) with admin tracking rules editor
- Nomination group tracker: CategoryGroupName as primary identifier,
  CategoryId stays as nullable legacy field; StreamerIdentity table
- Dynamic showact application form builder (AdminShowactFormBuilder,
  ShowactApplicationSchedule)
- Session idle timeout setting (AdminSessionTimeoutCard)
- Share URLs for X and Discord (SiteSettings, public extras)
- Workflow rules now stored per season (falls back to global SiteSettings)
- New admin routes: settings/access, settings/workflows, tracking-rules
- New admin review workspace with subcategory tabs
- AdminCategoriesView rebuilt with group/subcategory modals

Migrations (all additive):
- AddShareUrls, AddShowactDynamicForm, AddCategoryViewerRanges,
  AddSessionIdleTimeoutSettings, AddSeasonSubcategoryTemplates,
  AddNominationGroupTrackerIdentity, AddShowactApplicationSchedule,
  AddSeasonWorkflowRulesJson

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-06-28 23:32:21 +02:00
parent 4b2c5fa15d
commit b53c7fb736
178 changed files with 28933 additions and 1908 deletions
+38 -19
View File
@@ -12,6 +12,7 @@ public static partial class AuthEndpoints
{
private static async Task<IResult> DemoLogin(
HttpContext context,
IHostEnvironment environment,
AwardsDbContext db,
IConfiguration configuration,
DemoLoginRequest request,
@@ -23,11 +24,16 @@ public static partial class AuthEndpoints
var password = request.Password ?? string.Empty;
var settings = await db.SiteSettings.AsNoTracking().FirstOrDefaultAsync(item => item.Id == 1, context.RequestAborted);
var databaseDemoConfigured = settings is not null
&& (settings.DemoLoginManagedByDatabase || HasDatabaseDemoCredentials(settings));
&& settings.DemoLoginManagedByDatabase;
string twitchUserId;
string displayName;
bool credentialsMatch;
var fallbackConfiguredLogin = ReadDemoLoginIdentifier(configuration);
var fallbackConfiguredEmail = ReadDemoSetting(configuration, "Email", "VTSA_DEMO_ADMIN_EMAIL");
var fallbackConfiguredPassword = ReadDemoSetting(configuration, "Password", "VTSA_DEMO_ADMIN_PASSWORD");
var fallbackConfiguredTwitchUserId = ReadDemoSetting(configuration, "TwitchUserId", "VTSA_DEMO_ADMIN_TWITCH_ID");
var fallbackConfiguredDisplayName = ReadDemoSetting(configuration, "DisplayName", "VTSA_DEMO_ADMIN_DISPLAY_NAME");
if (databaseDemoConfigured && settings is not null)
{
@@ -53,6 +59,25 @@ public static partial class AuthEndpoints
&& DemoCredentialHasher.VerifyPassword(password, settings.DemoLoginPasswordHash, settings.DemoLoginPasswordSalt);
twitchUserId = settings.DemoLoginTwitchUserId.Trim();
displayName = settings.DemoLoginDisplayName.Trim();
if (!credentialsMatch
&& environment.IsDevelopment()
&& IsDemoLoginEnabled(configuration)
&& !string.IsNullOrWhiteSpace(fallbackConfiguredLogin)
&& !string.IsNullOrWhiteSpace(fallbackConfiguredPassword)
&& !string.IsNullOrWhiteSpace(fallbackConfiguredTwitchUserId)
&& !string.IsNullOrWhiteSpace(fallbackConfiguredDisplayName))
{
credentialsMatch = LoginMatchesIdentifier(
login,
fallbackConfiguredLogin,
fallbackConfiguredEmail,
fallbackConfiguredTwitchUserId,
fallbackConfiguredDisplayName)
&& DemoCredentialHasher.FixedTimePlainTextEquals(password, fallbackConfiguredPassword);
twitchUserId = fallbackConfiguredTwitchUserId.Trim();
displayName = fallbackConfiguredDisplayName.Trim();
}
}
else
{
@@ -61,16 +86,10 @@ public static partial class AuthEndpoints
return Results.NotFound();
}
var configuredLogin = ReadDemoLoginIdentifier(configuration);
var configuredEmail = ReadDemoSetting(configuration, "Email", "VTSA_DEMO_ADMIN_EMAIL");
var configuredPassword = ReadDemoSetting(configuration, "Password", "VTSA_DEMO_ADMIN_PASSWORD");
twitchUserId = ReadDemoSetting(configuration, "TwitchUserId", "VTSA_DEMO_ADMIN_TWITCH_ID");
displayName = ReadDemoSetting(configuration, "DisplayName", "VTSA_DEMO_ADMIN_DISPLAY_NAME");
if (string.IsNullOrWhiteSpace(configuredLogin)
|| string.IsNullOrWhiteSpace(configuredPassword)
|| string.IsNullOrWhiteSpace(twitchUserId)
|| string.IsNullOrWhiteSpace(displayName))
if (string.IsNullOrWhiteSpace(fallbackConfiguredLogin)
|| string.IsNullOrWhiteSpace(fallbackConfiguredPassword)
|| string.IsNullOrWhiteSpace(fallbackConfiguredTwitchUserId)
|| string.IsNullOrWhiteSpace(fallbackConfiguredDisplayName))
{
return Results.Json(
new { message = "Demo login is not fully configured." },
@@ -79,13 +98,13 @@ public static partial class AuthEndpoints
credentialsMatch = LoginMatchesIdentifier(
login,
configuredLogin,
configuredEmail,
twitchUserId,
displayName)
&& DemoCredentialHasher.FixedTimePlainTextEquals(password, configuredPassword);
twitchUserId = twitchUserId.Trim();
displayName = displayName.Trim();
fallbackConfiguredLogin,
fallbackConfiguredEmail,
fallbackConfiguredTwitchUserId,
fallbackConfiguredDisplayName)
&& DemoCredentialHasher.FixedTimePlainTextEquals(password, fallbackConfiguredPassword);
twitchUserId = fallbackConfiguredTwitchUserId.Trim();
displayName = fallbackConfiguredDisplayName.Trim();
}
if (!credentialsMatch)
@@ -137,7 +156,7 @@ public static partial class AuthEndpoints
await db.SaveChangesAsync(context.RequestAborted);
}
return Results.Ok(await ToAuthSessionDtoAsync(db, session, false, context.RequestAborted));
return Results.Ok(await ToAuthSessionDtoAsync(db, userSessionService, session, false, context.RequestAborted));
}
private static bool IsDemoLoginEnabled(IConfiguration configuration)