using StackExchange.Redis; using System.Text.Json; using System.Security.Cryptography; using System.Text; namespace GB5Shared.OTP { public class EmailOtpRedisService { private readonly IDatabase _db; private const int ExpiryMinutes = 5; private const int MaxAttempts = 5; public EmailOtpRedisService(IConnectionMultiplexer redis) { _db = redis.GetDatabase(); } private static string NormalizeEmail(string email) => email.Trim().ToLower(); private static string HashOtp(string otp) { using var sha = SHA256.Create(); var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(otp)); return Convert.ToBase64String(bytes); } public async Task SaveOtpAsync(string email, string otp, string ConnectionName) { email = NormalizeEmail(email); var key = $"OTP:EMAIL{ConnectionName}:{email}"; var hashedOtp = HashOtp(otp); var value = JsonSerializer.Serialize(new OtpData { OtpHash = hashedOtp, Attempts = 0 }); await _db.StringSetAsync( key, value, TimeSpan.FromMinutes(ExpiryMinutes) ); } public async Task VerifyOtpAsync(string email, string inputOtp,string ConnectionName) { email = NormalizeEmail(email); var key = $"OTP:EMAIL{ConnectionName}:{email}"; var value = await _db.StringGetAsync(key); if (value.IsNullOrEmpty) return false; var ttl = await _db.KeyTimeToLiveAsync(key); if (ttl == null) return false; var data = JsonSerializer.Deserialize((string)value!)!; if (data.Attempts >= MaxAttempts) return false; if (data.OtpHash != HashOtp(inputOtp)) { data.Attempts++; await _db.StringSetAsync( key, JsonSerializer.Serialize(data), ttl ); return false; } // OTP SUCCESS → DELETE await _db.KeyDeleteAsync(key); return true; } private class OtpData { public string OtpHash { get; set; } = ""; public int Attempts { get; set; } } } }