aboutsummaryrefslogtreecommitdiffstats
path: root/code/api
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2022-12-14 10:38:08 +0100
committerivarlovlie <git@ivarlovlie.no>2022-12-14 10:38:08 +0100
commit2f7da902c9afeb3df31f59fa6c16223990f51eb6 (patch)
treeda258139ba2ef663a3061f00bbf7257ef723b958 /code/api
parent0557de9f069dc620539409aced67e2ad61d25395 (diff)
downloadgreatoffice-2f7da902c9afeb3df31f59fa6c16223990f51eb6.tar.xz
greatoffice-2f7da902c9afeb3df31f59fa6c16223990f51eb6.zip
feat: Working email validation
Diffstat (limited to 'code/api')
-rw-r--r--code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs10
-rw-r--r--code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs2
-rw-r--r--code/api/src/Endpoints/Internal/Account/LoginRoute.cs4
-rw-r--r--code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs2
-rw-r--r--code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs2
-rw-r--r--code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs2
-rw-r--r--code/api/src/Endpoints/Internal/Root/ValidateRoute.cs28
-rw-r--r--code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs2
-rw-r--r--code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs2
-rw-r--r--code/api/src/Models/Database/MainAppDatabase.cs1
-rw-r--r--code/api/src/Models/Database/Queues/ValidationEmail.cs2
-rw-r--r--code/api/src/Models/Static/AppClaims.cs1
-rw-r--r--code/api/src/Models/Static/AppCookies.cs4
-rw-r--r--code/api/src/Program.cs3
-rw-r--r--code/api/src/Services/EmailValidationService.cs67
-rw-r--r--code/api/src/Services/MailService.cs2
-rw-r--r--code/api/src/Services/PasswordResetService.cs2
-rw-r--r--code/api/src/Services/UserService.cs75
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