82 lines
3.7 KiB
C#
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);
|
|
}
|
|
}
|