Files
GreenHomeBack/src/GreenHome.Infrastructure/Services/Auth/AuthService.cs

290 lines
9.4 KiB
C#

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<AuthService> _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<AuthService> logger)
{
_dbContext = dbContext;
_smsService = smsService;
_usageTracker = usageTracker;
_logger = logger;
_tokenSecret = configuration["Auth:TokenSecret"] ?? "greenhome-dev-secret-change-me";
}
public async Task<SendCodeResponse> 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<string, string> { { "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<VerifyCodeResponse> 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<bool> 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<UserDto?> 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}";
}
}