b53c7fb736
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>
202 lines
8.5 KiB
C#
202 lines
8.5 KiB
C#
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 partial class AuthEndpoints
|
|
{
|
|
private static async Task<IResult> DemoLogin(
|
|
HttpContext context,
|
|
IHostEnvironment environment,
|
|
AwardsDbContext db,
|
|
IConfiguration configuration,
|
|
DemoLoginRequest request,
|
|
IUserSessionService userSessionService,
|
|
IRiskFlagService riskFlagService,
|
|
IRiskRuleService riskRuleService)
|
|
{
|
|
var login = request.Login?.Trim() ?? request.Email?.Trim() ?? string.Empty;
|
|
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;
|
|
|
|
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)
|
|
{
|
|
if (!settings.DemoLoginEnabled)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
if (!HasDatabaseDemoCredentials(settings)
|
|
|| string.IsNullOrWhiteSpace(settings.DemoLoginTwitchUserId)
|
|
|| string.IsNullOrWhiteSpace(settings.DemoLoginDisplayName))
|
|
{
|
|
return Results.Json(
|
|
new { message = "Demo login is not fully configured." },
|
|
statusCode: StatusCodes.Status503ServiceUnavailable);
|
|
}
|
|
|
|
credentialsMatch = LoginMatchesIdentifier(
|
|
login,
|
|
settings.DemoLoginEmail,
|
|
settings.DemoLoginTwitchUserId,
|
|
settings.DemoLoginDisplayName)
|
|
&& 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
|
|
{
|
|
if (!IsDemoLoginEnabled(configuration))
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(fallbackConfiguredLogin)
|
|
|| string.IsNullOrWhiteSpace(fallbackConfiguredPassword)
|
|
|| string.IsNullOrWhiteSpace(fallbackConfiguredTwitchUserId)
|
|
|| string.IsNullOrWhiteSpace(fallbackConfiguredDisplayName))
|
|
{
|
|
return Results.Json(
|
|
new { message = "Demo login is not fully configured." },
|
|
statusCode: StatusCodes.Status503ServiceUnavailable);
|
|
}
|
|
|
|
credentialsMatch = LoginMatchesIdentifier(
|
|
login,
|
|
fallbackConfiguredLogin,
|
|
fallbackConfiguredEmail,
|
|
fallbackConfiguredTwitchUserId,
|
|
fallbackConfiguredDisplayName)
|
|
&& DemoCredentialHasher.FixedTimePlainTextEquals(password, fallbackConfiguredPassword);
|
|
twitchUserId = fallbackConfiguredTwitchUserId.Trim();
|
|
displayName = fallbackConfiguredDisplayName.Trim();
|
|
}
|
|
|
|
if (!credentialsMatch)
|
|
{
|
|
return Results.Unauthorized();
|
|
}
|
|
|
|
var requestMetadata = RequestMetadataReader.Read(context);
|
|
var session = await userSessionService.CreateSessionAsync(
|
|
twitchUserId,
|
|
displayName,
|
|
AdminRoles.Owner,
|
|
requestMetadata,
|
|
context.RequestAborted);
|
|
|
|
var rapidDemoLoginRule = await riskRuleService.GetRuleAsync("rapid_demo_login_ip", context.RequestAborted);
|
|
var recentSessionsFromIp = await userSessionService.CountRecentSessionsFromIpAsync(
|
|
requestMetadata.ClientIp,
|
|
DateTimeOffset.UtcNow.AddMinutes(-rapidDemoLoginRule.WindowMinutes),
|
|
context.RequestAborted);
|
|
|
|
if (rapidDemoLoginRule.Enabled && recentSessionsFromIp >= rapidDemoLoginRule.Threshold)
|
|
{
|
|
await riskFlagService.AddIfMissingAsync(
|
|
null,
|
|
session.TwitchUserId,
|
|
"login",
|
|
"rapid_demo_login_ip",
|
|
rapidDemoLoginRule.Severity,
|
|
"Mehrere Demo-Admin-Sessions wurden in kurzer Zeit von derselben IP erzeugt.",
|
|
requestMetadata,
|
|
new
|
|
{
|
|
recentSessionsFromIp,
|
|
threshold = rapidDemoLoginRule.Threshold,
|
|
windowMinutes = rapidDemoLoginRule.WindowMinutes,
|
|
entityLinks = new[]
|
|
{
|
|
new
|
|
{
|
|
label = "Audit-Log öffnen",
|
|
entityType = "session",
|
|
entityId = session.TwitchUserId,
|
|
to = $"/admin/users-logs?query={Uri.EscapeDataString(session.TwitchUserId)}",
|
|
},
|
|
},
|
|
},
|
|
context.RequestAborted);
|
|
await db.SaveChangesAsync(context.RequestAborted);
|
|
}
|
|
|
|
return Results.Ok(await ToAuthSessionDtoAsync(db, userSessionService, session, false, context.RequestAborted));
|
|
}
|
|
|
|
private static bool IsDemoLoginEnabled(IConfiguration configuration)
|
|
{
|
|
var rawValue = configuration["VTSA_DEMO_LOGIN_ENABLED"]
|
|
?? configuration["DemoAdmin:Enabled"];
|
|
|
|
return bool.TryParse(rawValue, out var enabled) && enabled;
|
|
}
|
|
|
|
private static string ReadDemoSetting(IConfiguration configuration, string key, string environmentKey) =>
|
|
configuration[environmentKey] ?? configuration[$"DemoAdmin:{key}"] ?? string.Empty;
|
|
|
|
private static string ReadDemoLoginIdentifier(IConfiguration configuration)
|
|
{
|
|
var configuredLogin = ReadDemoSetting(configuration, "Login", "VTSA_DEMO_ADMIN_LOGIN");
|
|
return string.IsNullOrWhiteSpace(configuredLogin)
|
|
? ReadDemoSetting(configuration, "Email", "VTSA_DEMO_ADMIN_EMAIL")
|
|
: configuredLogin;
|
|
}
|
|
|
|
private static bool HasDatabaseDemoCredentials(SiteSettings settings) =>
|
|
!string.IsNullOrWhiteSpace(settings.DemoLoginEmail)
|
|
&& !string.IsNullOrWhiteSpace(settings.DemoLoginPasswordHash)
|
|
&& !string.IsNullOrWhiteSpace(settings.DemoLoginPasswordSalt);
|
|
|
|
private static bool LoginMatchesIdentifier(string login, params string?[] validIdentifiers)
|
|
{
|
|
var normalizedLogin = NormalizeLoginIdentifier(login);
|
|
if (string.IsNullOrWhiteSpace(normalizedLogin))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return validIdentifiers
|
|
.Select(NormalizeLoginIdentifier)
|
|
.Any(identifier => string.Equals(normalizedLogin, identifier, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private static string NormalizeLoginIdentifier(string? value) =>
|
|
(value ?? string.Empty).Trim().TrimStart('@');
|
|
}
|