refactor: reorganize Infrastructure layer into folder structure with DI
This commit is contained in:
228
src/GreenHome.Infrastructure/Services/Auth/AuthService.cs
Normal file
228
src/GreenHome.Infrastructure/Services/Auth/AuthService.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
using GreenHome.Application;
|
||||
using GreenHome.Infrastructure.Data;
|
||||
using GreenHome.Sms.Ippanel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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 ILogger<AuthService> _logger;
|
||||
private const int CodeExpirationMinutes = 5;
|
||||
private const int ResendCooldownSeconds = 120;
|
||||
|
||||
public AuthService(
|
||||
GreenHomeDbContext dbContext,
|
||||
ISmsService smsService,
|
||||
ILogger<AuthService> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_smsService = smsService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SendCodeResponse> SendVerificationCodeAsync(
|
||||
SendCodeRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var mobile = NormalizeMobile(request.Mobile);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mobile) || mobile.Length != 11 || !mobile.StartsWith("09"))
|
||||
{
|
||||
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);
|
||||
|
||||
_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 = NormalizeMobile(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 = 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, mobile);
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> CanResendCodeAsync(string mobile, CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedMobile = NormalizeMobile(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();
|
||||
}
|
||||
|
||||
private static string GenerateToken(int userId, string mobile)
|
||||
{
|
||||
var data = $"{userId}:{mobile}:{DateTime.UtcNow:yyyyMMddHHmmss}";
|
||||
var bytes = Encoding.UTF8.GetBytes(data);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
|
||||
private static string NormalizeMobile(string mobile)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mobile))
|
||||
return string.Empty;
|
||||
|
||||
var normalized = new string(mobile.Where(char.IsDigit).ToArray());
|
||||
|
||||
if (normalized.StartsWith("9") && normalized.Length == 10)
|
||||
normalized = "0" + normalized;
|
||||
else if (normalized.StartsWith("0098") && normalized.Length == 13)
|
||||
normalized = "0" + normalized.Substring(3);
|
||||
else if (normalized.StartsWith("98") && normalized.Length == 12)
|
||||
normalized = "0" + normalized.Substring(2);
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user