summaryrefslogtreecommitdiffstats
path: root/server/src/Services/ForgotPasswordService.cs
blob: 6874d3768b451e92ab80e3051771bb25b0121249 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
namespace IOL.GreatOffice.Api.Services;

public class ForgotPasswordService
{
	private readonly AppDbContext _context;
	private readonly MailService _mailService;
	private readonly AppConfiguration _configuration;
	private readonly ILogger<ForgotPasswordService> _logger;


	public ForgotPasswordService(
			AppDbContext context,
			VaultService vaultService,
			ILogger<ForgotPasswordService> logger,
			MailService mailService
	) {
		_context = context;
		_configuration = vaultService.GetCurrentAppConfiguration();
		_logger = logger;
		_mailService = mailService;
	}

	public async Task<ForgotPasswordRequest> 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<bool> 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 portalUrl = _configuration.PORTAL_URL;
		var emailFromAddress = _configuration.EMAIL_FROM_ADDRESS;
		var emailFromDisplayName = _configuration.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.

{portalUrl}/#/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.");
	}
}