Files
step3/api/QuestionsApi/Services/AuthDataInitializer.cs
T

82 lines
3.7 KiB
C#

using MongoDB.Bson;
using MongoDB.Driver;
using QuestionsApi.Models;
namespace QuestionsApi.Services;
public sealed class AuthDataInitializer(
AuthUserStore users,
TokenBlacklistStore blacklist,
SteamAuthTransactionStore steamTransactions,
PasswordResetStore passwordResets,
ILogger<AuthDataInitializer> logger)
{
public async Task InitializeAsync()
{
await BackfillNormalizedEmailsAsync();
await users.Collection.Indexes.CreateManyAsync([
new CreateIndexModel<AuthUser>(
Builders<AuthUser>.IndexKeys.Ascending(user => user.Username),
new CreateIndexOptions { Unique = true }),
new CreateIndexModel<AuthUser>(
Builders<AuthUser>.IndexKeys.Ascending(user => user.SteamId),
new CreateIndexOptions<AuthUser>
{
Unique = true,
PartialFilterExpression = new BsonDocument("SteamId", new BsonDocument("$type", "string"))
}),
new CreateIndexModel<AuthUser>(
Builders<AuthUser>.IndexKeys.Ascending(user => user.NormalizedEmail),
new CreateIndexOptions<AuthUser>
{
Unique = true,
PartialFilterExpression = new BsonDocument("NormalizedEmail", new BsonDocument("$type", "string"))
})
]);
await blacklist.Collection.Indexes.CreateOneAsync(new CreateIndexModel<BlacklistedToken>(
Builders<BlacklistedToken>.IndexKeys.Ascending(token => token.ExpiresAt),
new CreateIndexOptions { ExpireAfter = TimeSpan.Zero }));
await steamTransactions.Collection.Indexes.CreateOneAsync(new CreateIndexModel<SteamAuthTransaction>(
Builders<SteamAuthTransaction>.IndexKeys.Ascending(transaction => transaction.ExpiresUtc),
new CreateIndexOptions { ExpireAfter = TimeSpan.Zero }));
await passwordResets.Collection.Indexes.CreateManyAsync([
new CreateIndexModel<PasswordResetRequest>(
Builders<PasswordResetRequest>.IndexKeys.Ascending(request => request.ExpiresUtc),
new CreateIndexOptions { ExpireAfter = TimeSpan.Zero, Name = "password_reset_expiry_ttl" }),
new CreateIndexModel<PasswordResetRequest>(
Builders<PasswordResetRequest>.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<AuthUser>.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);
}
}