aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--code/api/src/Data/Models/AppConfiguration.cs39
-rw-r--r--code/api/src/Services/MailService.cs144
-rw-r--r--code/api/src/Services/PasswordResetService.cs11
-rw-r--r--code/api/src/Services/VaultService.cs61
4 files changed, 163 insertions, 92 deletions
diff --git a/code/api/src/Data/Models/AppConfiguration.cs b/code/api/src/Data/Models/AppConfiguration.cs
index e2d7fa6..2a9afc2 100644
--- a/code/api/src/Data/Models/AppConfiguration.cs
+++ b/code/api/src/Data/Models/AppConfiguration.cs
@@ -54,17 +54,34 @@ public class AppConfiguration
/// </summary>
public string QUARTZ_DB_NAME { get; set; }
+ /// <summary>
+ /// API key to use when pushing logs to SEQ
+ /// </summary>
public string SEQ_API_KEY { get; set; }
+
+ /// <summary>
+ /// Url pointing to the seq instance that processes server logs
+ /// </summary>
public string SEQ_API_URL { get; set; }
- public string SMTP_HOST { get; set; }
- public string SMTP_PORT { get; set; }
- public string SMTP_USER { get; set; }
- public string SMTP_PASSWORD { get; set; }
+
+ /// <summary>
+ /// A token used when sending email via Postmakr
+ /// </summary>
+ public string POSTMARK_TOKEN { get; set; }
+
+ /// <summary>
+ /// The address to send emails from, needs to be setup as a sender in postmark
+ /// </summary>
public string EMAIL_FROM_ADDRESS { get; set; }
- public string EMAIL_FROM_DISPLAY_NAME { get; set; }
- public string GITHUB_CLIENT_ID { get; set; }
- public string GITHUB_CLIENT_SECRET { get; set; }
+
+ /// <summary>
+ /// The absolute url to the frontend app
+ /// </summary>
public string CANONICAL_FRONTEND_URL { get; set; }
+
+ /// <summary>
+ /// A random string used to encrypt/decrypt for general purposes
+ /// </summary>
public string APP_AES_KEY { get; set; }
/// <summary>
@@ -86,14 +103,8 @@ public class AppConfiguration
QUARTZ_DB_PASSWORD = QUARTZ_DB_PASSWORD.Obfuscate() ?? "",
SEQ_API_KEY = SEQ_API_KEY.Obfuscate() ?? "",
SEQ_API_URL,
- SMTP_HOST,
- SMTP_PORT,
- SMTP_USER = SMTP_USER.Obfuscate() ?? "",
- SMTP_PASSWORD = SMTP_PASSWORD.Obfuscate() ?? "",
+ POSTMARK_TOKEN = POSTMARK_TOKEN.Obfuscate() ?? "",
EMAIL_FROM_ADDRESS,
- EMAIL_FROM_DISPLAY_NAME,
- GITHUB_CLIENT_ID = GITHUB_CLIENT_ID.Obfuscate() ?? "",
- GITHUB_CLIENT_SECRET = GITHUB_CLIENT_SECRET.Obfuscate() ?? "",
APP_AES_KEY = APP_AES_KEY.Obfuscate() ?? "",
CERT1 = CERT1().PublicKey.Oid.FriendlyName,
CANONICAL_FRONTEND_URL
diff --git a/code/api/src/Services/MailService.cs b/code/api/src/Services/MailService.cs
index 1e565f5..6073f6e 100644
--- a/code/api/src/Services/MailService.cs
+++ b/code/api/src/Services/MailService.cs
@@ -3,50 +3,134 @@ 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;
+ private static string _postmarkToken;
+ private static string _fromEmail;
+ private readonly HttpClient _httpClient;
- public MailService(VaultService vaultService, ILogger<MailService> logger) {
+ public MailService(VaultService vaultService, ILogger<MailService> logger, HttpClient httpClient) {
var configuration = vaultService.GetCurrentAppConfiguration();
+ _postmarkToken = configuration.POSTMARK_TOKEN;
+ _fromEmail = configuration.EMAIL_FROM_ADDRESS;
_logger = logger;
- _emailHost = configuration.SMTP_HOST;
- _emailPort = Convert.ToInt32(configuration.SMTP_PORT);
- _emailUser = configuration.SMTP_USER;
- _emailPassword = configuration.SMTP_PASSWORD;
+ httpClient.DefaultRequestHeaders.Add("X-Postmark-Server-Token", _postmarkToken);
+ _httpClient = httpClient;
}
/// <summary>
/// Send an email.
/// </summary>
/// <param name="message"></param>
- public void SendMail(MailMessage message) {
+ public async Task SendMail(PostmarkEmail message) {
try {
- using var smtpClient = new SmtpClient {
- Host = _emailHost,
- EnableSsl = _emailPort == 587,
- Port = _emailPort,
- Credentials = new NetworkCredential {
- UserName = _emailUser,
- Password = _emailPassword,
+ if (message.MessageStream.IsNullOrWhiteSpace()) {
+ message.MessageStream = "outbound";
+ }
+
+ if (message.From.IsNullOrWhiteSpace() && _fromEmail.HasValue()) {
+ message.From = _fromEmail;
+ } else {
+ throw new ApplicationException("Not one from-email is available");
+ }
+
+ if (message.To.IsNullOrWhiteSpace()) {
+ throw new ArgumentNullException(nameof(message.To), "A recipient should be specified.");
+ }
+
+ if (!message.To.IsValidEmailAddress()) {
+ throw new ArgumentException(nameof(message.To), "To is not a valid email address");
+ }
+
+ if (message.HtmlBody.IsNullOrWhiteSpace() && message.TextBody.IsNullOrWhiteSpace()) {
+ throw new ArgumentNullException(nameof(message), "Both HtmlBody and TextBody is empty, nothing to send");
+ }
+
+ using var client = new HttpClient() {
+ DefaultRequestHeaders = {
+ {"X-Postmark-Server-Token", _postmarkToken},
}
};
- var configurationString = JsonSerializer.Serialize(new {
- smtpClient.Host,
- smtpClient.EnableSsl,
- 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);
+ // TODO: Log response if unsuccessful
+ await client.PostAsJsonAsync("https://api.postmarkapp.com/email", message);
} catch (Exception e) {
_logger.LogError(e, "An exception occured while trying to send an email");
}
}
+
+ private class PostmarkSendResponse
+ {
+ /// <summary>
+ /// The Message ID returned from Postmark.
+ /// </summary>
+ public Guid MessageID { get; set; }
+
+ /// <summary>
+ /// The message from the API.
+ /// In the event of an error, this message may contain helpful text.
+ /// </summary>
+ public string Message { get; set; }
+
+ /// <summary>
+ /// The time the request was received by Postmark.
+ /// </summary>
+ public DateTime SubmittedAt { get; set; }
+
+ /// <summary>
+ /// The recipient of the submitted request.
+ /// </summary>
+ public string To { get; set; }
+
+ /// <summary>
+ /// The error code returned from Postmark.
+ /// This does not map to HTTP status codes.
+ /// </summary>
+ public int ErrorCode { get; set; }
+ }
+
+ public class PostmarkEmail
+ {
+ /// <summary>
+ /// The message subject line.
+ /// </summary>
+ public string Subject { get; set; }
+
+ /// <summary>
+ /// The message body, if the message contains
+ /// </summary>
+ public string HtmlBody { get; set; }
+
+ /// <summary>
+ /// The message body, if the message is plain text.
+ /// </summary>
+ public string TextBody { get; set; }
+
+ /// <summary>
+ /// The message stream used to send this message.
+ /// </summary>
+ public string MessageStream { get; set; }
+
+ /// <summary>
+ /// The sender's email address.
+ /// </summary>
+ public string From { get; set; }
+
+ /// <summary>
+ /// Any recipients. Separate multiple recipients with a comma.
+ /// </summary>
+ public string To { get; set; }
+
+ /// <summary>
+ /// Any CC recipients. Separate multiple recipients with a comma.
+ /// </summary>
+ public string Cc { get; set; }
+
+ /// <summary>
+ /// Any BCC recipients. Separate multiple recipients with a comma.
+ /// </summary>
+ public string Bcc { get; set; }
+
+ /// <summary>
+ /// The email address to reply to. This is optional.
+ /// </summary>
+ public string ReplyTo { get; set; }
+ }
} \ No newline at end of file
diff --git a/code/api/src/Services/PasswordResetService.cs b/code/api/src/Services/PasswordResetService.cs
index cb4bc1e..8c8e32b 100644
--- a/code/api/src/Services/PasswordResetService.cs
+++ b/code/api/src/Services/PasswordResetService.cs
@@ -49,16 +49,11 @@ public class PasswordResetService
var request = new PasswordResetRequest(user);
_database.PasswordResetRequests.Add(request);
await _database.SaveChangesAsync(cancellationToken);
- var emailFromAddress = _configuration.EMAIL_FROM_ADDRESS;
- var emailFromDisplayName = _configuration.EMAIL_FROM_DISPLAY_NAME;
var zonedExpirationDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(request.ExpirationDate, requestTz.Id);
- var message = new MailMessage {
- From = new MailAddress(emailFromAddress, emailFromDisplayName),
- To = {
- new MailAddress(request.User.Username, request.User.DisplayName())
- },
+ var message = new MailService.PostmarkEmail() {
+ To = request.User.Username,
Subject = "Reset password - Greatoffice",
- Body = @$"
+ TextBody = @$"
Hi {user.Username}
Go to the following link to set a new password.
diff --git a/code/api/src/Services/VaultService.cs b/code/api/src/Services/VaultService.cs
index 8139985..d4ac775 100644
--- a/code/api/src/Services/VaultService.cs
+++ b/code/api/src/Services/VaultService.cs
@@ -10,8 +10,7 @@ public class VaultService
private readonly ILogger<VaultService> _logger;
private int CACHE_TTL { get; set; }
- public VaultService(HttpClient client, IConfiguration configuration, IMemoryCache cache, ILogger<VaultService> logger)
- {
+ public VaultService(HttpClient client, IConfiguration configuration, IMemoryCache cache, ILogger<VaultService> logger) {
var token = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_TOKEN);
var vaultUrl = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_URL);
CACHE_TTL = configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12);
@@ -25,20 +24,16 @@ public class VaultService
_logger = logger;
}
- public T Get<T>(string path)
- {
+ public T Get<T>(string path) {
var result = _cache.GetOrCreate(AppConstants.VAULT_CACHE_KEY,
- cacheEntry =>
- {
+ cacheEntry => {
cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(CACHE_TTL);
var getSecretResponse = _client.GetFromJsonAsync<GetSecretResponse<T>>("/v1/kv/data/" + path).Result;
- if (getSecretResponse == null)
- {
+ if (getSecretResponse == null) {
return default;
}
- Log.Debug("Setting new vault cache, " + new
- {
+ Log.Debug("Setting new vault cache, " + new {
PATH = path,
CACHE_TTL,
Data = JsonSerializer.Serialize(getSecretResponse.Data.Data)
@@ -48,42 +43,35 @@ public class VaultService
return result;
}
- public T Refresh<T>(string path)
- {
+ public T Refresh<T>(string path) {
_cache.Remove(AppConstants.VAULT_CACHE_KEY);
CACHE_TTL = _configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12);
return Get<T>(path);
}
- public async Task<RenewTokenResponse> RenewTokenAsync<T>(string token)
- {
+ public async Task<RenewTokenResponse> RenewTokenAsync<T>(string token) {
var response = await _client.PostAsJsonAsync("v1/auth/token/renew",
- new
- {
+ new {
Token = token
});
- if (response.IsSuccessStatusCode)
- {
+ if (response.IsSuccessStatusCode) {
return await response.Content.ReadFromJsonAsync<RenewTokenResponse>();
}
return default;
}
- public AppConfiguration GetCurrentAppConfiguration()
- {
+ public AppConfiguration GetCurrentAppConfiguration() {
#if DEBUG
var isInFlightMode = true;
- if (isInFlightMode)
- {
- return new AppConfiguration()
- {
- EMAIL_FROM_ADDRESS = "local@dev",
- EMAIL_FROM_DISPLAY_NAME = "Friendly neighbourhood developer",
+ if (isInFlightMode) {
+ return new AppConfiguration() {
+ EMAIL_FROM_ADDRESS = "heydev@greatoffice.life",
DB_HOST = "localhost",
DB_PORT = "5432",
DB_NAME = "greatoffice_ivar_dev",
DB_PASSWORD = "ivar123",
+ POSTMARK_TOKEN = "b530c311-45c7-43e5-aa48-f2c992886e51",
DB_USER = "postgres",
QUARTZ_DB_HOST = "localhost",
QUARTZ_DB_PORT = "5432",
@@ -97,8 +85,7 @@ public class VaultService
var path = _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET);
var result = Get<AppConfiguration>(path);
- var overwrites = new
- {
+ var overwrites = new {
DB_HOST = _configuration.GetValue("OVERWRITE_DB_HOST", string.Empty),
DB_PORT = _configuration.GetValue("OVERWRITE_DB_PORT", string.Empty),
DB_USER = _configuration.GetValue("OVERWRITE_DB_USER", string.Empty),
@@ -106,32 +93,27 @@ public class VaultService
DB_NAME = _configuration.GetValue("OVERWRITE_DB_NAME", string.Empty),
};
- if (overwrites.DB_HOST.HasValue())
- {
+ if (overwrites.DB_HOST.HasValue()) {
_logger.LogInformation("OVERWRITE_DB_HOST is specified, using it's value: {DB_HOST}", overwrites.DB_HOST);
result.DB_HOST = overwrites.DB_HOST;
}
- if (overwrites.DB_PORT.HasValue())
- {
+ if (overwrites.DB_PORT.HasValue()) {
_logger.LogInformation("OVERWRITE_DB_PORT is specified, using it's value: {DB_PORT}", overwrites.DB_PORT);
result.DB_PORT = overwrites.DB_PORT;
}
- if (overwrites.DB_USER.HasValue())
- {
+ if (overwrites.DB_USER.HasValue()) {
_logger.LogInformation("OVERWRITE_DB_USER is specified, using it's value: {DB_USER}", overwrites.DB_USER);
result.DB_USER = overwrites.DB_USER;
}
- if (overwrites.DB_PASSWORD.HasValue())
- {
+ if (overwrites.DB_PASSWORD.HasValue()) {
_logger.LogInformation("OVERWRITE_DB_PASSWORD is specified, using it's value: (redacted)");
result.DB_PASSWORD = overwrites.DB_PASSWORD;
}
- if (overwrites.DB_NAME.HasValue())
- {
+ if (overwrites.DB_NAME.HasValue()) {
_logger.LogInformation("OVERWRITE_DB_NAME is specified, using it's value: {DB_NAME}", overwrites.DB_NAME);
result.DB_NAME = overwrites.DB_NAME;
}
@@ -139,8 +121,7 @@ public class VaultService
return result;
}
- public AppConfiguration RefreshCurrentAppConfiguration()
- {
+ public AppConfiguration RefreshCurrentAppConfiguration() {
var path = _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET);
return Refresh<AppConfiguration>(path);
}