using Backend.Domain; using Backend.Security; using Microsoft.EntityFrameworkCore; namespace Backend.Data; public static class TeamAccountBootstrapper { private const string SeedActor = "system:team-seed"; public static async Task EnsureAsync(AwardsDbContext db, IConfiguration configuration) { var ownerSeed = BuildSeed( configuration, sectionName: "Owner", environmentPrefix: "VTSA_TEAM_OWNER", defaultLogin: "jayuhime", defaultDisplayName: "Jayuhime", role: AdminRoles.Owner, fallbackPassword: configuration["VTSA_DEMO_ADMIN_PASSWORD"] ?? configuration["DemoAdmin:Password"]); var creatorSeed = BuildSeed( configuration, sectionName: "Creator", environmentPrefix: "VTSA_TEAM_CREATOR", defaultLogin: "sleepy_bao", defaultDisplayName: "sleepy_bao", role: AdminRoles.Creator, fallbackPassword: null); foreach (var seed in new[] { ownerSeed, creatorSeed }) { await EnsureMemberAsync(db, seed); } await db.SaveChangesAsync(); } private static async Task EnsureMemberAsync(AwardsDbContext db, TeamAccountSeed seed) { if (seed.Credentials is null) { return; } var member = await db.TeamMembers.FirstOrDefaultAsync(item => item.Login == seed.Login); if (member is null) { db.TeamMembers.Add(new TeamMember { Login = seed.Login, DisplayName = seed.DisplayName, Role = seed.Role, PasswordHash = seed.Credentials.Value.Hash, PasswordSalt = seed.Credentials.Value.Salt, MustChangePassword = false, IsActive = true, CreatedByTwitchId = SeedActor, CreatedAt = DateTimeOffset.UtcNow, }); return; } var changed = false; if (!string.Equals(member.DisplayName, seed.DisplayName, StringComparison.Ordinal)) { member.DisplayName = seed.DisplayName; changed = true; } if (!string.Equals(member.Role, seed.Role, StringComparison.Ordinal)) { member.Role = seed.Role; changed = true; } if (!member.IsActive) { member.IsActive = true; changed = true; } if (changed) { member.UpdatedAt = DateTimeOffset.UtcNow; member.UpdatedByTwitchId = SeedActor; } } private static TeamAccountSeed BuildSeed( IConfiguration configuration, string sectionName, string environmentPrefix, string defaultLogin, string defaultDisplayName, string role, string? fallbackPassword) { var login = ReadSetting(configuration, sectionName, environmentPrefix, "Login") ?? defaultLogin; var displayName = ReadSetting(configuration, sectionName, environmentPrefix, "DisplayName") ?? defaultDisplayName; return new TeamAccountSeed( NormalizeLogin(login), displayName.Trim(), role, ResolveCredentials(configuration, sectionName, environmentPrefix, fallbackPassword)); } private static TeamSeedCredentials? ResolveCredentials( IConfiguration configuration, string sectionName, string environmentPrefix, string? fallbackPassword) { var configuredHash = ReadSetting(configuration, sectionName, environmentPrefix, "PasswordHash"); var configuredSalt = ReadSetting(configuration, sectionName, environmentPrefix, "PasswordSalt"); if (!string.IsNullOrWhiteSpace(configuredHash) && !string.IsNullOrWhiteSpace(configuredSalt)) { return new TeamSeedCredentials(configuredHash.Trim(), configuredSalt.Trim()); } var password = ReadSetting(configuration, sectionName, environmentPrefix, "Password") ?? fallbackPassword; if (string.IsNullOrWhiteSpace(password)) { return null; } var credentials = DemoCredentialHasher.HashPassword(password); return new TeamSeedCredentials(credentials.Hash, credentials.Salt); } private static string? ReadSetting(IConfiguration configuration, string sectionName, string environmentPrefix, string key) { var raw = configuration[$"{environmentPrefix}_{ToEnvironmentKey(key)}"] ?? configuration[$"TeamSeed:{sectionName}:{key}"]; return string.IsNullOrWhiteSpace(raw) ? null : raw; } private static string ToEnvironmentKey(string key) { return string.Concat(key.Select((character, index) => index > 0 && char.IsUpper(character) ? $"_{character}" : character.ToString())).ToUpperInvariant(); } private static string NormalizeLogin(string value) => value.Trim().TrimStart('@').ToLowerInvariant(); private sealed record TeamAccountSeed(string Login, string DisplayName, string Role, TeamSeedCredentials? Credentials); private readonly record struct TeamSeedCredentials(string Hash, string Salt); }