namespace IOL.GreatOffice.Api.Services; public class ForgotPasswordService { private readonly AppDbContext _context; private readonly MailService _mailService; private readonly IConfiguration _configuration; private readonly ILogger _logger; public ForgotPasswordService( AppDbContext context, IConfiguration configuration, ILogger logger, MailService mailService ) { _context = context; _configuration = configuration; _logger = logger; _mailService = mailService; } public async Task GetRequestAsync(Guid id, CancellationToken cancellationToken = default) { var request = await _context.ForgotPasswordRequests .Include(c => c.User) .SingleOrDefaultAsync(c => c.Id == id, cancellationToken); if (request == default) { return default; } _logger.LogInformation($"Found forgot password request for user: {request.User.Username}, expires at {request.ExpirationDate} (in {request.ExpirationDate.Subtract(DateTime.UtcNow).Minutes} minutes)."); return request; } public async Task FullFillRequestAsync(Guid id, string newPassword, CancellationToken cancellationToken = default) { var request = await GetRequestAsync(id, cancellationToken); if (request == default) { throw new ForgotPasswordRequestNotFoundException("Request with id: " + id + " was not found"); } var user = _context.Users.SingleOrDefault(c => c.Id == request.User.Id); if (user == default) { throw new UserNotFoundException("User with id: " + request.User.Id + " was not found"); } user.HashAndSetPassword(newPassword); _context.Users.Update(user); await _context.SaveChangesAsync(cancellationToken); _logger.LogInformation($"Fullfilled forgot password request for user: {request.User.Username}"); await DeleteRequestsForUserAsync(user.Id, cancellationToken); return true; } public async Task AddRequestAsync(User user, TimeZoneInfo requestTz, CancellationToken cancellationToken = default) { await DeleteRequestsForUserAsync(user.Id, cancellationToken); var request = new ForgotPasswordRequest(user); _context.ForgotPasswordRequests.Add(request); await _context.SaveChangesAsync(cancellationToken); var accountsUrl = _configuration.GetValue(AppEnvironmentVariables.ACCOUNTS_URL); var emailFromAddress = _configuration.GetValue(AppEnvironmentVariables.EMAIL_FROM_ADDRESS); var emailFromDisplayName = _configuration.GetValue(AppEnvironmentVariables.EMAIL_FROM_DISPLAY_NAME); var zonedExpirationDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(request.ExpirationDate, requestTz.Id); var message = new MailMessage { From = new MailAddress(emailFromAddress, emailFromDisplayName), To = { new MailAddress(user.Username) }, Subject = "Time Tracker - Forgot password request", Body = @$" Hi {user.Username} Go to the following link to set a new password. {accountsUrl}/#/reset-password?id={request.Id} The link expires at {zonedExpirationDate:yyyy-MM-dd hh:mm}. If you did not request a password reset, no action is required. " }; #pragma warning disable 4014 Task.Run(() => { #pragma warning restore 4014 _mailService.SendMail(message); _logger.LogInformation($"Added forgot password request for user: {request.User.Username}, expires in {request.ExpirationDate.Subtract(DateTime.UtcNow)}."); }, cancellationToken); } public async Task DeleteRequestsForUserAsync(Guid userId, CancellationToken cancellationToken = default) { var requestsToRemove = _context.ForgotPasswordRequests.Where(c => c.UserId == userId).ToList(); if (!requestsToRemove.Any()) return; _context.ForgotPasswordRequests.RemoveRange(requestsToRemove); await _context.SaveChangesAsync(cancellationToken); _logger.LogInformation($"Deleted {requestsToRemove.Count} forgot password requests for user: {userId}."); } public async Task DeleteStaleRequestsAsync(CancellationToken cancellationToken = default) { var deleteCount = 0; foreach (var request in _context.ForgotPasswordRequests) { if (!request.IsExpired) { continue; } _context.ForgotPasswordRequests.Remove(request); deleteCount++; _logger.LogInformation($"Marking forgot password request with id: {request.Id} for deletion, expiration date was {request.ExpirationDate}."); } await _context.SaveChangesAsync(cancellationToken); _logger.LogInformation($"Deleted {deleteCount} stale forgot password requests."); } }