From a640703f2da8815dc26ad1600a6f206be1624379 Mon Sep 17 00:00:00 2001 From: ivarlovlie Date: Wed, 1 Jun 2022 22:10:32 +0200 Subject: feat: Initial after clean slate --- server/src/Services/ForgotPasswordService.cs | 115 +++++++++++++++++++++++++++ server/src/Services/MailService.cs | 52 ++++++++++++ server/src/Services/UserService.cs | 50 ++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 server/src/Services/ForgotPasswordService.cs create mode 100644 server/src/Services/MailService.cs create mode 100644 server/src/Services/UserService.cs (limited to 'server/src/Services') diff --git a/server/src/Services/ForgotPasswordService.cs b/server/src/Services/ForgotPasswordService.cs new file mode 100644 index 0000000..de38b29 --- /dev/null +++ b/server/src/Services/ForgotPasswordService.cs @@ -0,0 +1,115 @@ +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."); + } +} diff --git a/server/src/Services/MailService.cs b/server/src/Services/MailService.cs new file mode 100644 index 0000000..b271de4 --- /dev/null +++ b/server/src/Services/MailService.cs @@ -0,0 +1,52 @@ +namespace IOL.GreatOffice.Api.Services; + +public class MailService +{ + private readonly ILogger _logger; + private static string _emailHost; + private static int _emailPort; + private static string _emailUser; + private static string _emailPassword; + + /// + /// Provides methods to send email. + /// + /// + /// + public MailService(IConfiguration configuration, ILogger logger) { + _logger = logger; + _emailHost = configuration.GetValue(AppEnvironmentVariables.SMTP_HOST); + _emailPort = configuration.GetValue(AppEnvironmentVariables.SMTP_PORT); + _emailUser = configuration.GetValue(AppEnvironmentVariables.SMTP_USER); + _emailPassword = configuration.GetValue(AppEnvironmentVariables.SMTP_PASSWORD); + } + + /// + /// Send an email. + /// + /// + public void SendMail(MailMessage message) { + using var smtpClient = new SmtpClient { + Host = _emailHost, + EnableSsl = _emailPort == 587, + Port = _emailPort, + Credentials = new NetworkCredential { + UserName = _emailUser, + Password = _emailPassword, + } + }; + var configurationString = JsonSerializer.Serialize(new { + Host = smtpClient.Host, + EnableSsl = smtpClient.EnableSsl, + Port = smtpClient.Port, + UserName = _emailUser.HasValue() ? "**REDACTED**" : "**MISSING**", + Password = _emailPassword.HasValue() ? "**REDACTED**" : "**MISSING**", + }, + new JsonSerializerOptions { + WriteIndented = true + }); + _logger.LogDebug("SmtpClient was instansiated with the following configuration\n" + configurationString); + + smtpClient.Send(message); + } +} diff --git a/server/src/Services/UserService.cs b/server/src/Services/UserService.cs new file mode 100644 index 0000000..9b531de --- /dev/null +++ b/server/src/Services/UserService.cs @@ -0,0 +1,50 @@ +namespace IOL.GreatOffice.Api.Services; + +public class UserService +{ + private readonly ForgotPasswordService _forgotPasswordService; + + /// + /// Provides methods to perform common operations on user data. + /// + /// + public UserService(ForgotPasswordService forgotPasswordService) { + _forgotPasswordService = forgotPasswordService; + } + + /// + /// Log in a user. + /// + /// + /// + /// + public async Task LogInUser(HttpContext httpContext, User user, bool persist = false) { + var claims = new List { + new(AppClaims.USER_ID, user.Id.ToString()), + new(AppClaims.NAME, user.Username), + }; + + var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + var principal = new ClaimsPrincipal(identity); + var authenticationProperties = new AuthenticationProperties { + AllowRefresh = true, + IssuedUtc = DateTimeOffset.UtcNow, + }; + + if (persist) { + authenticationProperties.ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(6); + authenticationProperties.IsPersistent = true; + } + + await httpContext.SignInAsync(principal, authenticationProperties); + await _forgotPasswordService.DeleteRequestsForUserAsync(user.Id); + } + + /// + /// Log out a user. + /// + /// + public async Task LogOutUser(HttpContext httpContext) { + await httpContext.SignOutAsync(); + } +} -- cgit v1.3