summaryrefslogtreecommitdiffstats
path: root/server/src/Services
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/Services')
-rw-r--r--server/src/Services/ForgotPasswordService.cs115
-rw-r--r--server/src/Services/MailService.cs52
-rw-r--r--server/src/Services/UserService.cs50
3 files changed, 217 insertions, 0 deletions
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<ForgotPasswordService> _logger;
+
+
+ public ForgotPasswordService(
+ AppDbContext context,
+ IConfiguration configuration,
+ ILogger<ForgotPasswordService> logger,
+ MailService mailService
+ ) {
+ _context = context;
+ _configuration = configuration;
+ _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 accountsUrl = _configuration.GetValue<string>(AppEnvironmentVariables.ACCOUNTS_URL);
+ var emailFromAddress = _configuration.GetValue<string>(AppEnvironmentVariables.EMAIL_FROM_ADDRESS);
+ var emailFromDisplayName = _configuration.GetValue<string>(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<MailService> _logger;
+ private static string _emailHost;
+ private static int _emailPort;
+ private static string _emailUser;
+ private static string _emailPassword;
+
+ /// <summary>
+ /// Provides methods to send email.
+ /// </summary>
+ /// <param name="configuration"></param>
+ /// <param name="logger"></param>
+ public MailService(IConfiguration configuration, ILogger<MailService> logger) {
+ _logger = logger;
+ _emailHost = configuration.GetValue<string>(AppEnvironmentVariables.SMTP_HOST);
+ _emailPort = configuration.GetValue<int>(AppEnvironmentVariables.SMTP_PORT);
+ _emailUser = configuration.GetValue<string>(AppEnvironmentVariables.SMTP_USER);
+ _emailPassword = configuration.GetValue<string>(AppEnvironmentVariables.SMTP_PASSWORD);
+ }
+
+ /// <summary>
+ /// Send an email.
+ /// </summary>
+ /// <param name="message"></param>
+ 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;
+
+ /// <summary>
+ /// Provides methods to perform common operations on user data.
+ /// </summary>
+ /// <param name="forgotPasswordService"></param>
+ public UserService(ForgotPasswordService forgotPasswordService) {
+ _forgotPasswordService = forgotPasswordService;
+ }
+
+ /// <summary>
+ /// Log in a user.
+ /// </summary>
+ /// <param name="httpContext"></param>
+ /// <param name="user"></param>
+ /// <param name="persist"></param>
+ public async Task LogInUser(HttpContext httpContext, User user, bool persist = false) {
+ var claims = new List<Claim> {
+ 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);
+ }
+
+ /// <summary>
+ /// Log out a user.
+ /// </summary>
+ /// <param name="httpContext"></param>
+ public async Task LogOutUser(HttpContext httpContext) {
+ await httpContext.SignOutAsync();
+ }
+}