aboutsummaryrefslogtreecommitdiffstats
path: root/code/api/src/Services/PasswordResetService.cs
blob: a179e1031f34dc6477a15803cb1c09c3b861c7ed (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
using IOL.GreatOffice.Api.Models.Database;

namespace IOL.GreatOffice.Api.Services;

public class PasswordResetService
{
    private readonly MainAppDatabase _database;
    private readonly MailService _mailService;
    private readonly AppConfiguration _configuration;
    private readonly ILogger<PasswordResetService> _logger;
    private readonly IStringLocalizer<SharedResources> _localizer;

    public PasswordResetService(
        MainAppDatabase database,
        VaultService vaultService,
        ILogger<PasswordResetService> logger,
        MailService mailService, IStringLocalizer<SharedResources> localizer) {
        _database = database;
        _configuration = vaultService.GetCurrentAppConfiguration();
        _logger = logger;
        _mailService = mailService;
        _localizer = localizer;
    }

    public async Task<PasswordResetRequest> GetRequestAsync(Guid id, CancellationToken cancellationToken = default) {
        var request = await _database.PasswordResetRequests
            .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<FulfillPasswordResetRequestResult> FulfillRequestAsync(Guid id, string newPassword, CancellationToken cancellationToken = default) {
        var request = await GetRequestAsync(id, cancellationToken);
        if (request == default) return FulfillPasswordResetRequestResult.REQUEST_NOT_FOUND;
        var user = _database.Users.FirstOrDefault(c => c.Id == request.User.Id);
        if (user == default) return FulfillPasswordResetRequestResult.USER_NOT_FOUND;
        user.HashAndSetPassword(newPassword);
        _database.Users.Update(user);
        await _database.SaveChangesAsync(cancellationToken);
        _logger.LogInformation($"Fullfilled password reset request for user: {request.User.Username}");
        await DeleteRequestsForUserAsync(user.Id, cancellationToken);
        return FulfillPasswordResetRequestResult.FULFILLED;
    }

    public async Task AddRequestAsync(User user, TimeZoneInfo requestTz, CancellationToken cancellationToken = default) {
        await DeleteRequestsForUserAsync(user.Id, cancellationToken);
        var request = new PasswordResetRequest(user);
        _database.PasswordResetRequests.Add(request);
        await _database.SaveChangesAsync(cancellationToken);
        var zonedExpirationDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(request.ExpirationDate, requestTz.Id);
        var message = new MailService.PostmarkEmail() {
            To = request.User.Username,
            Subject = _localizer["Reset password - Greatoffice"],
            TextBody = _localizer["""
Hi {0},

Go to the following link to set a new password.

{1}/reset-password/{2}

The link expires at {3}.
If you did not request a password reset, no action is required.
""", user.DisplayName(true), _configuration.CANONICAL_FRONTEND_URL, request.Id, zonedExpirationDate.ToString("yyyy-MM-dd hh:mm")]
        };

#pragma warning disable 4014
        Task.Run(() => {
#pragma warning restore 4014
                _mailService.SendMailAsync(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 = _database.PasswordResetRequests.Where(c => c.UserId == userId).ToList();
        if (!requestsToRemove.Any()) return;
        _database.PasswordResetRequests.RemoveRange(requestsToRemove);
        await _database.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 _database.PasswordResetRequests.Where(c => c.IsExpired)) {
            if (!request.IsExpired) {
                continue;
            }

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

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