using System.Security.Cryptography; using GameAuthApi.Models; using MongoDB.Driver; using System; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; namespace GameAuthApi.Services; public class PasswordResetService { private readonly IMongoCollection _challenges; public PasswordResetService(IConfiguration config) { var client = new MongoClient(config["MongoDB:ConnectionString"]); var database = client.GetDatabase(config["MongoDB:DatabaseName"]); _challenges = database.GetCollection("PasswordResetChallenges"); } public virtual async Task CreateAsync(string userId) { await _challenges.UpdateManyAsync(x => x.UserId == userId && !x.Used, Builders.Update.Set(x => x.Used, true)); var code = RandomNumberGenerator.GetInt32(0, 1_000_000).ToString("D6"); await _challenges.InsertOneAsync(new PasswordResetChallenge { UserId = userId, CodeHash = BCrypt.Net.BCrypt.HashPassword(code), CreatedUtc = DateTime.UtcNow, ExpiresUtc = DateTime.UtcNow.AddMinutes(15) }); return code; } public virtual async Task ConsumeAsync(string userId, string code) { var challenge = await _challenges.Find(x => x.UserId == userId && !x.Used && x.ExpiresUtc > DateTime.UtcNow) .SortByDescending(x => x.CreatedUtc).FirstOrDefaultAsync(); if (challenge == null || challenge.FailedAttempts >= 5) return false; if (!BCrypt.Net.BCrypt.Verify(code, challenge.CodeHash)) { await _challenges.UpdateOneAsync(x => x.Id == challenge.Id, Builders.Update.Inc(x => x.FailedAttempts, 1)); return false; } await _challenges.UpdateOneAsync(x => x.Id == challenge.Id, Builders.Update.Set(x => x.Used, true)); return true; } public async Task EnsureIndexesAsync() => await _challenges.Indexes.CreateOneAsync( new CreateIndexModel( Builders.IndexKeys.Ascending(x => x.ExpiresUtc), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero, Name = "password_reset_ttl" })); }