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 const string TeamSessionPrefix = "team:"; private const int MinTeamPasswordLength = 10; private static async Task TeamLogin( HttpContext context, TeamLoginRequest? request, AwardsDbContext db, IUserSessionService userSessionService) { 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.BadRequest(new { message = "Login und Passwort sind erforderlich." }); } var member = await db.TeamMembers.FirstOrDefaultAsync(item => item.Login == login, context.RequestAborted); if (member is null || !member.IsActive || !DemoCredentialHasher.VerifyPassword(password, member.PasswordHash, member.PasswordSalt)) { return Results.Unauthorized(); } member.LastLoginAt = DateTimeOffset.UtcNow; var session = await userSessionService.CreateSessionAsync( BuildTeamSessionId(member.Login), member.DisplayName, member.Role, RequestMetadataReader.Read(context), context.RequestAborted); await db.SaveChangesAsync(context.RequestAborted); return Results.Ok(await ToAuthSessionDtoAsync(db, userSessionService, session, member.MustChangePassword, context.RequestAborted)); } private static async Task ChangePassword( HttpContext context, ChangePasswordRequest request, AwardsDbContext db, IUserSessionService userSessionService) { var session = await userSessionService.ResolveSessionAsync(context, context.RequestAborted); if (session is null) { return Results.Unauthorized(); } var member = await FindTeamMemberForSessionAsync(db, session, context.RequestAborted); if (member is null || !member.IsActive) { return Results.BadRequest(new { message = "Dieser Account nutzt keinen aktiven Team-Login." }); } var currentPassword = request.CurrentPassword ?? string.Empty; var newPassword = request.NewPassword?.Trim() ?? string.Empty; if (!DemoCredentialHasher.VerifyPassword(currentPassword, member.PasswordHash, member.PasswordSalt)) { return Results.BadRequest(new { message = "Das aktuelle Passwort stimmt nicht." }); } if (newPassword.Length < MinTeamPasswordLength) { return Results.BadRequest(new { message = $"Das neue Passwort muss mindestens {MinTeamPasswordLength} Zeichen lang sein." }); } if (DemoCredentialHasher.FixedTimePlainTextEquals(currentPassword, newPassword)) { return Results.BadRequest(new { message = "Das neue Passwort muss sich vom aktuellen Passwort unterscheiden." }); } var credentials = DemoCredentialHasher.HashPassword(newPassword); member.PasswordHash = credentials.Hash; member.PasswordSalt = credentials.Salt; member.MustChangePassword = false; member.UpdatedAt = DateTimeOffset.UtcNow; member.UpdatedByTwitchId = session.TwitchUserId; session.DisplayName = member.DisplayName; session.Role = AdminRoles.Normalize(member.Role); await db.SaveChangesAsync(context.RequestAborted); return Results.Ok(await ToAuthSessionDtoAsync(db, userSessionService, session, false, context.RequestAborted)); } private static string BuildTeamSessionId(string login) => $"{TeamSessionPrefix}{login}"; internal static string ReadTeamLoginFromSession(string twitchUserId) => twitchUserId.StartsWith(TeamSessionPrefix, StringComparison.OrdinalIgnoreCase) ? NormalizeTeamLogin(twitchUserId[TeamSessionPrefix.Length..]) : string.Empty; internal static async Task FindTeamMemberForSessionAsync( AwardsDbContext db, UserSession session, CancellationToken cancellationToken = default) { var login = ReadTeamLoginFromSession(session.TwitchUserId); if (!string.IsNullOrWhiteSpace(login)) { return await db.TeamMembers.FirstOrDefaultAsync(item => item.Login == login, cancellationToken); } var twitchUserId = NormalizeTwitchUserId(session.TwitchUserId); return string.IsNullOrWhiteSpace(twitchUserId) ? null : await db.TeamMembers.FirstOrDefaultAsync(item => item.BoundTwitchUserId == twitchUserId, cancellationToken); } private static string NormalizeTeamLogin(string? value) => (value ?? string.Empty).Trim().TrimStart('@').ToLowerInvariant(); private static string NormalizeTwitchUserId(string? value) => (value ?? string.Empty).Trim().TrimStart('@').ToLowerInvariant(); }