aboutsummaryrefslogtreecommitdiffstats
path: root/code/api/src/Services/PasswordResetService.cs
blob: 1b4f147a09c6afc21a1647d3e5bb7211dfcbb405 (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 PasswordResetService
{
    private readonly AppDbContext _context;
    private readonly MailService _mailService;
    private readonly AppConfiguration _configuration;
    private readonly ILogger<PasswordResetService> _logger;


    public PasswordResetService(
        AppDbContext context,
        VaultService vaultService,
        ILogger<PasswordResetService> 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 password reset request for user: {request.User.Username}, expires at {request.ExpirationDate} (in {request.ExpirationDate.Subtract(AppDateTime.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 password reset 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 = "Reset password - Greatoffice",
            Body = @$"
Hi {user.Username}

Go to the following link to set a new password.

{portalUrl}/reset-password/{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 password reset request for user: {request.User.Username}, expires in {request.ExpirationDate.Subtract(AppDateTime.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} password reset requests for user: {userId}.");
    }


    public async Task DeleteStaleRequestsAsync(CancellationToken cancellationToken = default) {
        var deleteCount = 0;
        foreach (var request in _context.ForgotPasswordRequests.Where(c => c.IsExpired)) {
            if (!request.IsExpired) {
                continue;
            }

            _context.ForgotPasswordRequests.Remove(request);
            deleteCount++;
            _logger.LogInformation($"Marking password reset request with id: {request.Id} for deletion, expiration date was {request.ExpirationDate}.");
        }

        await _context.SaveChangesAsync(cancellationToken);
        _logger.LogInformation($"Deleted {deleteCount} stale password reset requests.");
    }
}