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 DemoLogin( HttpContext context, 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 || HasDatabaseDemoCredentials(settings)); string twitchUserId; string displayName; bool credentialsMatch; 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(); } else { if (!IsDemoLoginEnabled(configuration)) { 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)) { return Results.Json( new { message = "Demo login is not fully configured." }, statusCode: StatusCodes.Status503ServiceUnavailable); } credentialsMatch = LoginMatchesIdentifier( login, configuredLogin, configuredEmail, twitchUserId, displayName) && DemoCredentialHasher.FixedTimePlainTextEquals(password, configuredPassword); twitchUserId = twitchUserId.Trim(); displayName = displayName.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, 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('@'); }