using MongoDB.Bson; using MongoDB.Driver; using QuestionsApi.Models; using System.Text.RegularExpressions; namespace QuestionsApi.Services; public sealed class AuthUserStore(MongoDatabaseProvider mongo) { private readonly IMongoCollection _users = mongo.Database.GetCollection("Users"); public async Task GetByUsernameAsync(string username) => await _users.Find(user => user.Username == username.Trim()).FirstOrDefaultAsync(); public async Task GetByIdAsync(string id) => await _users.Find(user => user.Id == id).FirstOrDefaultAsync(); public async Task GetBySteamIdAsync(string steamId) => await _users.Find(user => user.SteamId == steamId).FirstOrDefaultAsync(); public async Task GetUniqueByEmailAsync(string email) { var normalized = AuthValidation.NormalizeEmail(email); var filter = Builders.Filter.Eq(user => user.NormalizedEmail, normalized) | Builders.Filter.Regex(user => user.Email, new BsonRegularExpression($"^{Regex.Escape(normalized)}$", "i")); var matches = await _users.Find(filter).Limit(2).ToListAsync(); return matches.Count == 1 ? matches[0] : null; } public async Task ResolveUniqueLoginAsync(string login) { var username = await GetByUsernameAsync(login); return username ?? await GetUniqueByEmailAsync(login); } public async Task> GetByLoginCandidatesAsync(string login) { var username = await GetByUsernameAsync(login); if (username is not null) return [username]; var email = await GetUniqueByEmailAsync(login); return email is null ? [] : [email]; } public Task EmailExistsAsync(string email, string? excludingUserId = null) { var normalized = AuthValidation.NormalizeEmail(email); var filter = (Builders.Filter.Eq(user => user.NormalizedEmail, normalized) | Builders.Filter.Regex(user => user.Email, new BsonRegularExpression($"^{Regex.Escape(normalized)}$", "i"))); if (!string.IsNullOrWhiteSpace(excludingUserId)) filter &= Builders.Filter.Ne(user => user.Id, excludingUserId); return _users.Find(filter).AnyAsync(); } public Task CreateAsync(AuthUser user) => _users.InsertOneAsync(user); public Task UpdateAsync(AuthUser user) => _users.ReplaceOneAsync(value => value.Id == user.Id, user); public async Task TryLinkSteamAsync(string userId, string steamId, string refreshToken, DateTime refreshExpiry) { var filter = Builders.Filter.Eq(user => user.Id, userId) & (Builders.Filter.Eq(user => user.SteamId, null) | Builders.Filter.Eq(user => user.SteamId, steamId)); var update = Builders.Update .Set(user => user.SteamId, steamId) .Set(user => user.RefreshToken, refreshToken) .Set(user => user.RefreshTokenExpiry, refreshExpiry); return (await _users.UpdateOneAsync(filter, update)).MatchedCount == 1; } public async Task ResetPasswordAsync(string userId, string passwordHash) { var update = Builders.Update .Set(user => user.PasswordHash, passwordHash) .Set(user => user.RefreshToken, null) .Set(user => user.RefreshTokenExpiry, null) .Inc(user => user.AuthVersion, 1); return (await _users.UpdateOneAsync(user => user.Id == userId, update)).ModifiedCount == 1; } public Task DeleteAsync(string id) => _users.DeleteOneAsync(user => user.Id == id); public Task> GetAllAsync() => _users.Find(FilterDefinition.Empty).ToListAsync(); internal IMongoCollection Collection => _users; }