using MongoDB.Bson; using MongoDB.Driver; using QuestionsApi.Models; namespace QuestionsApi.Services; public sealed class AuthDataInitializer( AuthUserStore users, TokenBlacklistStore blacklist, SteamAuthTransactionStore steamTransactions, ExternalIdentityStore externalIdentities, ExternalAuthTransactionStore externalTransactions, PasswordResetStore passwordResets, ILogger logger) { public async Task InitializeAsync() { await BackfillNormalizedEmailsAsync(); await externalIdentities.BackfillSteamAsync(logger); await externalIdentities.EnsureIndexesAsync(); await externalTransactions.EnsureIndexesAsync(); await users.Collection.Indexes.CreateManyAsync([ new CreateIndexModel( Builders.IndexKeys.Ascending(user => user.Username), new CreateIndexOptions { Unique = true }), new CreateIndexModel( Builders.IndexKeys.Ascending(user => user.SteamId), new CreateIndexOptions { Unique = true, PartialFilterExpression = new BsonDocument("SteamId", new BsonDocument("$type", "string")) }), new CreateIndexModel( Builders.IndexKeys.Ascending(user => user.NormalizedEmail), new CreateIndexOptions { Unique = true, PartialFilterExpression = new BsonDocument("NormalizedEmail", new BsonDocument("$type", "string")) }) ]); await blacklist.Collection.Indexes.CreateOneAsync(new CreateIndexModel( Builders.IndexKeys.Ascending(token => token.ExpiresAt), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero })); await steamTransactions.Collection.Indexes.CreateOneAsync(new CreateIndexModel( Builders.IndexKeys.Ascending(transaction => transaction.ExpiresUtc), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero })); await passwordResets.Collection.Indexes.CreateManyAsync([ new CreateIndexModel( Builders.IndexKeys.Ascending(request => request.ExpiresUtc), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero, Name = "password_reset_expiry_ttl" }), new CreateIndexModel( Builders.IndexKeys.Ascending(request => request.UserId) .Descending(request => request.CreatedUtc), new CreateIndexOptions { Name = "password_reset_user_created" }) ]); } private async Task BackfillNormalizedEmailsAsync() { var candidates = await users.Collection.Find(user => user.Email != null && user.Email != "" && user.NormalizedEmail == null).ToListAsync(); var groups = candidates.GroupBy(user => AuthValidation.NormalizeEmail(user.Email!), StringComparer.Ordinal) .ToList(); var ambiguous = 0; foreach (var group in groups) { var existing = await users.Collection.Find(user => user.NormalizedEmail == group.Key).Limit(1).AnyAsync(); if (group.Count() != 1 || existing) { ambiguous += group.Count(); continue; } var user = group.Single(); await users.Collection.UpdateOneAsync(value => value.Id == user.Id, Builders.Update .Set(value => value.Email, group.Key) .Set(value => value.NormalizedEmail, group.Key)); } if (ambiguous > 0) logger.LogWarning( "Skipped normalized-email backfill for {Count} users with ambiguous legacy email addresses; username recovery remains available", ambiguous); } }