diff options
| author | ivarlovlie <git@ivarlovlie.no> | 2022-12-14 10:38:08 +0100 |
|---|---|---|
| committer | ivarlovlie <git@ivarlovlie.no> | 2022-12-14 10:38:08 +0100 |
| commit | 2f7da902c9afeb3df31f59fa6c16223990f51eb6 (patch) | |
| tree | da258139ba2ef663a3061f00bbf7257ef723b958 /code | |
| parent | 0557de9f069dc620539409aced67e2ad61d25395 (diff) | |
| download | greatoffice-2f7da902c9afeb3df31f59fa6c16223990f51eb6.tar.xz greatoffice-2f7da902c9afeb3df31f59fa6c16223990f51eb6.zip | |
feat: Working email validation
Diffstat (limited to 'code')
18 files changed, 106 insertions, 105 deletions
diff --git a/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs index c114bb8..a145c38 100644 --- a/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Localization; - namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; public class CreateAccountRoute : RouteBaseAsync.WithRequest<CreateAccountRoute.Payload>.WithActionResult @@ -7,11 +5,13 @@ public class CreateAccountRoute : RouteBaseAsync.WithRequest<CreateAccountRoute. private readonly MainAppDatabase _database; private readonly UserService _userService; private readonly IStringLocalizer<SharedResources> _localizer; + private readonly EmailValidationService _emailValidation; - public CreateAccountRoute(UserService userService, MainAppDatabase database, IStringLocalizer<SharedResources> localizer) { + public CreateAccountRoute(UserService userService, MainAppDatabase database, IStringLocalizer<SharedResources> localizer, EmailValidationService emailValidation) { _userService = userService; _database = database; _localizer = localizer; + _emailValidation = emailValidation; } public class Payload @@ -45,8 +45,8 @@ public class CreateAccountRoute : RouteBaseAsync.WithRequest<CreateAccountRoute. user.HashAndSetPassword(request.Password); _database.Users.Add(user); await _database.SaveChangesAsync(cancellationToken); - await _userService.LogInUser(HttpContext, user); - Task.Run(() => _userService.SendValidationEmail(user), cancellationToken); + await _userService.LogInUserAsync(HttpContext, user); + await _emailValidation.SendValidationEmailAsync(user); return Ok(); } }
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs index 56ff9c6..01cad3f 100644 --- a/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs @@ -26,7 +26,7 @@ public class CreateInitialAccountRoute : RouteBaseAsync.WithoutRequest.WithActio user.HashAndSetPassword("ivar123"); _database.Users.Add(user); await _database.SaveChangesAsync(cancellationToken); - await _userService.LogInUser(HttpContext, user); + await _userService.LogInUserAsync(HttpContext, user); return Redirect("/"); } }
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/LoginRoute.cs b/code/api/src/Endpoints/Internal/Account/LoginRoute.cs index eaebc2a..8a3dff4 100644 --- a/code/api/src/Endpoints/Internal/Account/LoginRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/LoginRoute.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Localization; - namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; public class LoginRoute : RouteBaseAsync.WithRequest<LoginRoute.Payload>.WithActionResult @@ -29,7 +27,7 @@ public class LoginRoute : RouteBaseAsync.WithRequest<LoginRoute.Payload>.WithAct return KnownProblem(_localizer["Invalid username or password"]); } - await _userService.LogInUser(HttpContext, user, request.Persist); + await _userService.LogInUserAsync(HttpContext, user, request.Persist); return Ok(); } }
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs index c75e750..1081240 100644 --- a/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Localization; - namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; public class UpdateAccountRoute : RouteBaseAsync.WithRequest<UpdateAccountRoute.Payload>.WithActionResult diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs index 9a22ab3..c6ed417 100644 --- a/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Localization; - namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; public class CreateResetRequestRoute : RouteBaseAsync.WithRequest<CreateResetRequestRoute.Payload>.WithActionResult diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs index 8c7ce03..a8797b8 100644 --- a/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Localization; - namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; public class FulfillResetRequestRoute : RouteBaseAsync.WithRequest<FulfillResetRequestRoute.Payload>.WithActionResult diff --git a/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs b/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs index 428a1a2..8f0882d 100644 --- a/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs +++ b/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs @@ -2,13 +2,15 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; public class ValidateRoute : RouteBaseSync.WithRequest<ValidateRoute.QueryParams>.WithActionResult { - private readonly UserService _userService; - private readonly string _continueTo; + private readonly EmailValidationService _emailValidation; + private readonly string CanonicalFrontendUrl; + private readonly ILogger<ValidateRoute> _logger; - public ValidateRoute(UserService userService, VaultService vaultService) { - _userService = userService; + public ValidateRoute(VaultService vaultService, EmailValidationService emailValidation, ILogger<ValidateRoute> logger) { + _emailValidation = emailValidation; + _logger = logger; var c = vaultService.GetCurrentAppConfiguration(); - _continueTo = c.CANONICAL_FRONTEND_URL + "/portal?msg=emailValidated"; + CanonicalFrontendUrl = c.CANONICAL_FRONTEND_URL; } public class QueryParams @@ -19,7 +21,19 @@ public class ValidateRoute : RouteBaseSync.WithRequest<ValidateRoute.QueryParams [HttpGet("~/_/validate")] public override ActionResult Handle([FromQuery] QueryParams request) { - _userService.FulfillEmailValidationRequest(request.Id, LoggedInUser.Id); - return Redirect(_continueTo); + var isFulfilled = _emailValidation.FulfillEmailValidationRequest(request.Id, LoggedInUser.Id); + if (!isFulfilled) { + _logger.LogError("Email validation fulfillment failed for request {requestId} and user {userId}", request.Id, LoggedInUser.Id); + return StatusCode(400, $""" +<html> +<body> +<h3>The validation could not be completed</h3> +<p>We are working on fixing this, in the meantime, have patience.</p> +<a href="{CanonicalFrontendUrl}">Click here to go back to {CanonicalFrontendUrl}</a> +</body> +"""); + } + + return Redirect(CanonicalFrontendUrl + "/portal?msg=emailValidated"); } }
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs b/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs index b20b404..e58aa37 100644 --- a/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs +++ b/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Localization; - namespace IOL.GreatOffice.Api.Endpoints.V1.Customers; public class CreateCustomerRoute : RouteBaseAsync.WithRequest<CreateCustomerPayload>.WithActionResult diff --git a/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs b/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs index 04a3a9a..795422a 100644 --- a/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs +++ b/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Localization; - namespace IOL.GreatOffice.Api.Endpoints.V1.Projects; public class CreateProjectRoute : RouteBaseAsync.WithRequest<CreateProjectPayload>.WithActionResult diff --git a/code/api/src/Models/Database/MainAppDatabase.cs b/code/api/src/Models/Database/MainAppDatabase.cs index 33e5dcd..2b42fe0 100644 --- a/code/api/src/Models/Database/MainAppDatabase.cs +++ b/code/api/src/Models/Database/MainAppDatabase.cs @@ -1,4 +1,3 @@ -using IOL.GreatOffice.Api.Data.Database.Queues; using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; namespace IOL.GreatOffice.Api.Data.Database; diff --git a/code/api/src/Models/Database/Queues/ValidationEmail.cs b/code/api/src/Models/Database/Queues/ValidationEmail.cs index 8ca8c5d..0457768 100644 --- a/code/api/src/Models/Database/Queues/ValidationEmail.cs +++ b/code/api/src/Models/Database/Queues/ValidationEmail.cs @@ -1,4 +1,4 @@ -namespace IOL.GreatOffice.Api.Data.Database.Queues; +namespace IOL.GreatOffice.Api.Data.Database; public class ValidationEmail { diff --git a/code/api/src/Models/Static/AppClaims.cs b/code/api/src/Models/Static/AppClaims.cs index 8b6d3a8..6569700 100644 --- a/code/api/src/Models/Static/AppClaims.cs +++ b/code/api/src/Models/Static/AppClaims.cs @@ -4,5 +4,4 @@ public static class AppClaims { public const string USER_ID = "user_id"; public const string NAME = "name"; - public const string GITHUB_ACCESS_TOKEN = ""; } diff --git a/code/api/src/Models/Static/AppCookies.cs b/code/api/src/Models/Static/AppCookies.cs index 57204dd..e307b83 100644 --- a/code/api/src/Models/Static/AppCookies.cs +++ b/code/api/src/Models/Static/AppCookies.cs @@ -2,6 +2,6 @@ namespace IOL.GreatOffice.Api.Data.Static; public static class AppCookies { - public static readonly string Locale = "go_locale"; - public static readonly string Session = "go_session"; + public const string Locale = "go_locale"; + public const string Session = "go_session"; }
\ No newline at end of file diff --git a/code/api/src/Program.cs b/code/api/src/Program.cs index c771210..3da1111 100644 --- a/code/api/src/Program.cs +++ b/code/api/src/Program.cs @@ -19,13 +19,13 @@ global using Microsoft.OpenApi.Models; global using Microsoft.AspNetCore.Authentication.Cookies; global using Microsoft.AspNetCore.Builder; global using Microsoft.AspNetCore.DataProtection; -global using Microsoft.AspNetCore.Hosting; global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Http; global using Microsoft.AspNetCore.Authentication; global using Microsoft.AspNetCore.Mvc; global using Microsoft.EntityFrameworkCore; global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.Localization; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Logging; @@ -52,6 +52,7 @@ public static class Program builder.Services.AddScoped<PasswordResetService>(); builder.Services.AddScoped<UserService>(); builder.Services.AddScoped<TenantService>(); + builder.Services.AddScoped<EmailValidationService>(); builder.Services.AddSingleton<VaultService>(); builder.Services.AddHttpClient<VaultService>(); builder.Services.AddHttpClient<MailService>(); diff --git a/code/api/src/Services/EmailValidationService.cs b/code/api/src/Services/EmailValidationService.cs new file mode 100644 index 0000000..875e3ee --- /dev/null +++ b/code/api/src/Services/EmailValidationService.cs @@ -0,0 +1,67 @@ +namespace IOL.GreatOffice.Api.Services; + +public class EmailValidationService +{ + private readonly IStringLocalizer<SharedResources> _localizer; + private readonly MainAppDatabase _database; + private readonly MailService _mailService; + private readonly ILogger<EmailValidationService> _logger; + private readonly string EmailValidationUrl; + + public EmailValidationService(IStringLocalizer<SharedResources> localizer, MainAppDatabase database, MailService mailService, ILogger<EmailValidationService> logger, VaultService vaultService) { + _localizer = localizer; + _database = database; + _mailService = mailService; + _logger = logger; + var configuration = vaultService.GetCurrentAppConfiguration(); + EmailValidationUrl = configuration.CANONICAL_BACKEND_URL + "/_/validate"; + } + + public bool FulfillEmailValidationRequest(Guid id, Guid userId) { + var item = _database.ValidationEmails.FirstOrDefault(c => c.Id == id); + if (item == default) { + _logger.LogDebug("Did not find email validation request with id: {requestId}", id); + return false; + } + + if (item.UserId != userId) { + _logger.LogInformation("An unknown user tried to validate the email validation request {requestId}", id); + return false; + } + + var user = _database.Users.FirstOrDefault(c => c.Id == item.UserId); + if (user == default) { + _database.ValidationEmails.Remove(item); + _database.SaveChanges(); + _logger.LogInformation("Deleting request {requestId} because user does not exist anymore", id); + return false; + } + + user.EmailLastValidated = AppDateTime.UtcNow; + _database.ValidationEmails.Remove(item); + _database.Users.Update(user); + _database.SaveChanges(); + _logger.LogInformation("Successfully validated the email for user {userId}", user.Id); + return true; + } + + public async Task SendValidationEmailAsync(User user) { + var queueItem = new ValidationEmail() { + UserId = user.Id, + Id = Guid.NewGuid() + }; + var email = new MailService.PostmarkEmail() { + To = user.Username, + Subject = _localizer["Greatoffice Email Validation"], + TextBody = _localizer[""" +Hello {0}, + +Validate your email address by opening this link in a browser {1} +""", user.DisplayName(true), EmailValidationUrl + "?id=" + queueItem.Id] + }; + queueItem.EmailSentAt = AppDateTime.UtcNow; + _database.ValidationEmails.Add(queueItem); + await _database.SaveChangesAsync(); + Task.Run(async () => await _mailService.SendMail(email)); + } +}
\ No newline at end of file diff --git a/code/api/src/Services/MailService.cs b/code/api/src/Services/MailService.cs index 4d789dd..e724b89 100644 --- a/code/api/src/Services/MailService.cs +++ b/code/api/src/Services/MailService.cs @@ -47,7 +47,7 @@ public class MailService var response = await _httpClient.PostAsJsonAsync("https://api.postmarkapp.com/email", message); _logger.LogInformation("Postmark returned with message: {0}", (await response.Content.ReadFromJsonAsync<PostmarkSendResponse>()).Message); } catch (Exception e) { - _logger.LogError(e, "An exception occured while trying to send an email"); + _logger.LogError(e, "A silent exception occured while trying to send an email"); } } diff --git a/code/api/src/Services/PasswordResetService.cs b/code/api/src/Services/PasswordResetService.cs index 1897d44..3bf6c84 100644 --- a/code/api/src/Services/PasswordResetService.cs +++ b/code/api/src/Services/PasswordResetService.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Localization; - namespace IOL.GreatOffice.Api.Services; public class PasswordResetService diff --git a/code/api/src/Services/UserService.cs b/code/api/src/Services/UserService.cs index 4fd2aa4..9c6132c 100644 --- a/code/api/src/Services/UserService.cs +++ b/code/api/src/Services/UserService.cs @@ -1,34 +1,17 @@ -using IOL.GreatOffice.Api.Data.Database.Queues; -using Microsoft.Extensions.Localization; - namespace IOL.GreatOffice.Api.Services; public class UserService { private readonly PasswordResetService _passwordResetService; - private readonly MailService _mailService; private readonly ILogger<UserService> _logger; - private readonly IStringLocalizer<SharedResources> _localizer; - private readonly MainAppDatabase _database; - private readonly string EmailValidationUrl; - public UserService(PasswordResetService passwordResetService, MailService mailService, IStringLocalizer<SharedResources> localizer, VaultService vaultService, MainAppDatabase database, ILogger<UserService> logger) { + public UserService(PasswordResetService passwordResetService, ILogger<UserService> logger) { _passwordResetService = passwordResetService; - _mailService = mailService; - _localizer = localizer; - _database = database; _logger = logger; - var configuration = vaultService.GetCurrentAppConfiguration(); - EmailValidationUrl = configuration.CANONICAL_BACKEND_URL + "/_/validate"; } - 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); + public async Task LogInUserAsync(HttpContext httpContext, User user, bool persist = false) { + var identity = new ClaimsIdentity(user.DefaultClaims(), CookieAuthenticationDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); var authenticationProperties = new AuthenticationProperties { AllowRefresh = true, @@ -42,59 +25,11 @@ public class UserService await httpContext.SignInAsync(principal, authenticationProperties); await _passwordResetService.DeleteRequestsForUserAsync(user.Id); - _logger.LogInformation("Logged in user {0}", user.Id); + _logger.LogInformation("Logged in user {userId}", user.Id); } public async Task LogOutUser(HttpContext httpContext) { await httpContext.SignOutAsync(); - _logger.LogInformation("Logged out user {0}", httpContext.User.FindFirst(AppClaims.USER_ID)); - } - - public bool FulfillEmailValidationRequest(Guid id, Guid userId) { - var item = _database.ValidationEmails.FirstOrDefault(c => c.Id == id); - if (item == default) { - _logger.LogDebug("Did not find email validation request with id: {0}", id); - return false; - } - - if (item.UserId != userId) { - _logger.LogInformation("An unknown user tried to validate the email validation request {0}"); - return false; - } - - var user = _database.Users.FirstOrDefault(c => c.Id == item.UserId); - if (user == default) { - _database.ValidationEmails.Remove(item); - _database.SaveChanges(); - _logger.LogInformation("Deleting request {0} because user does not exist anymore"); - return false; - } - - user.EmailLastValidated = DateTime.UtcNow; - user.SetModified(); - _database.ValidationEmails.Remove(item); - _database.SaveChanges(); - _logger.LogInformation("Successfully validated the email for user {0}", user.Id); - return true; - } - - public async Task SendValidationEmail(User user) { - var queueItem = new ValidationEmail() { - UserId = user.Id, - Id = Guid.NewGuid() - }; - var email = new MailService.PostmarkEmail() { - To = user.Username, - Subject = _localizer["Greatoffice Email Validation"], - TextBody = _localizer[""" -Hello, {0}. - -Validate your email address by opening this link in a browser {1} -""", user.DisplayName(true), EmailValidationUrl + "?id=" + queueItem.Id] - }; - await _mailService.SendMail(email); - queueItem.EmailSentAt = DateTime.UtcNow; - _database.ValidationEmails.Add(queueItem); - await _database.SaveChangesAsync(); + _logger.LogInformation("Logged out user {userId}", httpContext.User.FindFirst(AppClaims.USER_ID)); } }
\ No newline at end of file |
