using GreenHome.Application; using GreenHome.Infrastructure.Data; using GreenHome.Infrastructure.Services.Usage; using GreenHome.Sms.Ippanel; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Security.Cryptography; using System.Text; namespace GreenHome.Infrastructure.Services.Auth; public sealed class AuthService : IAuthService { private readonly GreenHomeDbContext _dbContext; private readonly ISmsService _smsService; private readonly UsageTracker _usageTracker; private readonly ILogger _logger; private readonly string _tokenSecret; private const int CodeExpirationMinutes = 5; private const int ResendCooldownSeconds = 120; public AuthService( GreenHomeDbContext dbContext, ISmsService smsService, UsageTracker usageTracker, IConfiguration configuration, ILogger logger) { _dbContext = dbContext; _smsService = smsService; _usageTracker = usageTracker; _logger = logger; _tokenSecret = configuration["Auth:TokenSecret"] ?? "greenhome-dev-secret-change-me"; } public async Task SendVerificationCodeAsync( SendCodeRequest request, CancellationToken cancellationToken) { var mobile = Domain.MobileNormalizer.Normalize(request.Mobile); if (mobile.Length == 0) { return new SendCodeResponse { Success = false, Message = "شماره موبایل معتبر نیست" }; } var existingUser = await _dbContext.Users .AsNoTracking() .FirstOrDefaultAsync(u => u.Mobile == mobile, cancellationToken); if (existingUser != null && existingUser.IsDisabled) { return new SendCodeResponse { Success = false, Message = "حساب شما غیرفعال شده است. لطفاً با پشتیبانی تماس بگیرید." }; } var canResend = await CanResendCodeAsync(mobile, cancellationToken); if (!canResend) { var lastCode = await _dbContext.VerificationCodes .Where(v => v.Mobile == mobile && !v.IsUsed) .OrderByDescending(v => v.CreatedAt) .FirstOrDefaultAsync(cancellationToken); if (lastCode != null) { var secondsRemaining = (int)(ResendCooldownSeconds - (DateTime.UtcNow - lastCode.CreatedAt).TotalSeconds); return new SendCodeResponse { Success = false, Message = $"لطفاً {secondsRemaining} ثانیه دیگر دوباره تلاش کنید", ResendAfterSeconds = secondsRemaining > 0 ? secondsRemaining : 0 }; } } var code = GenerateCode(); var previousCodes = await _dbContext.VerificationCodes .Where(v => v.Mobile == mobile && !v.IsUsed) .ToListAsync(cancellationToken); foreach (var prevCode in previousCodes) { prevCode.IsUsed = true; prevCode.UsedAt = DateTime.UtcNow; } var verificationCode = new Domain.VerificationCode { Mobile = mobile, Code = code, CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddMinutes(CodeExpirationMinutes), IsUsed = false }; _dbContext.VerificationCodes.Add(verificationCode); await _dbContext.SaveChangesAsync(cancellationToken); try { await _smsService.SendPatternSmsAsync(new PatternSmsRequest { Recipients = [mobile], PatternCode = "ruvpjx7lajne1dx", Variables = new Dictionary { { "code", code } } }, cancellationToken); if (existingUser != null) await _usageTracker.RecordAsync(existingUser.Id, UsageKind.Sms, cancellationToken: cancellationToken); _logger.LogInformation("Verification code sent to {Mobile}", mobile); } catch (Exception ex) { _logger.LogError(ex, "Failed to send SMS to {Mobile}", mobile); } return new SendCodeResponse { Success = true, Message = "کد فعال‌سازی ارسال شد", ResendAfterSeconds = ResendCooldownSeconds }; } public async Task VerifyCodeAsync( VerifyCodeRequest request, CancellationToken cancellationToken) { var mobile = Domain.MobileNormalizer.Normalize(request.Mobile); var code = request.Code.Trim(); if (string.IsNullOrWhiteSpace(mobile) || string.IsNullOrWhiteSpace(code) || code.Length != 4) { return new VerifyCodeResponse { Success = false, Message = "اطلاعات وارد شده معتبر نیست" }; } var verificationCode = await _dbContext.VerificationCodes .Where(v => v.Mobile == mobile && v.Code == code && !v.IsUsed && v.ExpiresAt > DateTime.UtcNow) .OrderByDescending(v => v.CreatedAt) .FirstOrDefaultAsync(cancellationToken); if (verificationCode == null) { return new VerifyCodeResponse { Success = false, Message = "کد وارد شده معتبر نیست یا منقضی شده است" }; } verificationCode.IsUsed = true; verificationCode.UsedAt = DateTime.UtcNow; var user = await _dbContext.Users .FirstOrDefaultAsync(u => u.Mobile == mobile, cancellationToken); if (user != null && user.IsDisabled) { return new VerifyCodeResponse { Success = false, Message = "حساب شما غیرفعال شده است. لطفاً با پشتیبانی تماس بگیرید." }; } if (user == null) { user = new Domain.User { Mobile = mobile, CreatedAt = DateTime.UtcNow, LastLoginAt = DateTime.UtcNow }; _dbContext.Users.Add(user); } else { user.LastLoginAt = DateTime.UtcNow; } await _dbContext.SaveChangesAsync(cancellationToken); var token = GenerateToken(user.Id); return new VerifyCodeResponse { Success = true, Message = "ورود موفقیت‌آمیز بود", Token = token, User = new UserDto { Id = user.Id, Mobile = user.Mobile, Name = user.Name, Family = user.Family, Role = user.Role, IsDisabled = user.IsDisabled } }; } public async Task CanResendCodeAsync(string mobile, CancellationToken cancellationToken) { var normalizedMobile = Domain.MobileNormalizer.Normalize(mobile); var lastCode = await _dbContext.VerificationCodes .Where(v => v.Mobile == normalizedMobile && !v.IsUsed) .OrderByDescending(v => v.CreatedAt) .FirstOrDefaultAsync(cancellationToken); if (lastCode == null) return true; var elapsed = (DateTime.UtcNow - lastCode.CreatedAt).TotalSeconds; return elapsed >= ResendCooldownSeconds; } private static string GenerateCode() { var random = new Random(); return random.Next(1000, 9999).ToString(); } public async Task GetUserByTokenAsync(string token, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(token)) return null; var parts = token.Split('.'); if (parts.Length != 3) return null; if (!int.TryParse(parts[0], out var userId) || !long.TryParse(parts[1], out var expiresAt)) return null; if (expiresAt < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) return null; var payload = $"{parts[0]}.{parts[1]}"; var expectedSignature = Convert.ToBase64String( HMACSHA256.HashData(Encoding.UTF8.GetBytes(_tokenSecret), Encoding.UTF8.GetBytes(payload))); if (!CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expectedSignature), Encoding.UTF8.GetBytes(parts[2]))) return null; var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); if (user == null) return null; if (user.IsDisabled) return null; return new UserDto { Id = user.Id, Mobile = user.Mobile, Name = user.Name, Family = user.Family, Role = user.Role, IsDisabled = user.IsDisabled }; } private string GenerateToken(int userId) { var expiresAt = DateTimeOffset.UtcNow.AddDays(30).ToUnixTimeSeconds(); var payload = $"{userId}.{expiresAt}"; var signature = Convert.ToBase64String( HMACSHA256.HashData(Encoding.UTF8.GetBytes(_tokenSecret), Encoding.UTF8.GetBytes(payload))); return $"{payload}.{signature}"; } }