diff options
Diffstat (limited to 'code')
| -rw-r--r-- | code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs | 8 | ||||
| -rw-r--r-- | code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs | 15 | ||||
| -rw-r--r-- | code/api/src/Endpoints/Internal/Root/ValidateRoute.cs | 12 | ||||
| -rw-r--r-- | code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs | 21 | ||||
| -rw-r--r-- | code/api/src/Jobs/JobRegister.cs | 9 | ||||
| -rw-r--r-- | code/api/src/Jobs/VaultTokenRenewalJob.cs | 24 | ||||
| -rw-r--r-- | code/api/src/Models/Misc/AppConfiguration.cs | 102 | ||||
| -rw-r--r-- | code/api/src/Models/Static/AppEnvironmentVariables.cs | 34 | ||||
| -rw-r--r-- | code/api/src/Program.cs | 22 | ||||
| -rw-r--r-- | code/api/src/Services/EmailValidationService.cs | 27 | ||||
| -rw-r--r-- | code/api/src/Services/MailService.cs | 41 | ||||
| -rw-r--r-- | code/api/src/Services/PasswordResetService.cs | 5 | ||||
| -rw-r--r-- | code/api/src/Services/VaultService.cs | 219 | ||||
| -rw-r--r-- | code/api/src/Utilities/BasicAuthenticationHandler.cs | 7 | ||||
| -rw-r--r-- | code/api/src/Utilities/ConfigurationExtensions.cs | 85 |
15 files changed, 176 insertions, 455 deletions
diff --git a/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs b/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs index a02dbb8..14a4186 100644 --- a/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs +++ b/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs @@ -2,18 +2,14 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; public class ReadConfigurationRoute : RouteBaseSync.WithoutRequest.WithActionResult { - private readonly VaultService _vaultService; - - public ReadConfigurationRoute(VaultService vaultService) + public ReadConfigurationRoute() { - _vaultService = vaultService; } [AllowAnonymous] [HttpGet("~/_/configuration")] public override ActionResult Handle() { - var config = _vaultService.GetCurrentAppConfiguration(); - return Content(JsonSerializer.Serialize(config.GetPublicObject()), "application/json"); + return Content(JsonSerializer.Serialize(Program.AppConfiguration.GetPublicObject()), "application/json"); } }
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs b/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs deleted file mode 100644 index 2bbfd8f..0000000 --- a/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; - -public class RefreshConfigurationRoute : RouteBaseAsync.WithoutRequest.WithoutResult -{ - private readonly VaultService _vaultService; - - public RefreshConfigurationRoute(VaultService vaultService) { - _vaultService = vaultService; - } - - [HttpGet("~/_/refresh-configuration")] - public override async Task HandleAsync(CancellationToken cancellationToken = default) { - await _vaultService.RefreshCurrentAppConfigurationAsync(); - } -}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs b/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs index 8f0882d..d8ec85a 100644 --- a/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs +++ b/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs @@ -6,11 +6,11 @@ public class ValidateRoute : RouteBaseSync.WithRequest<ValidateRoute.QueryParams private readonly string CanonicalFrontendUrl; private readonly ILogger<ValidateRoute> _logger; - public ValidateRoute(VaultService vaultService, EmailValidationService emailValidation, ILogger<ValidateRoute> logger) { + public ValidateRoute(EmailValidationService emailValidation, ILogger<ValidateRoute> logger) + { _emailValidation = emailValidation; _logger = logger; - var c = vaultService.GetCurrentAppConfiguration(); - CanonicalFrontendUrl = c.CANONICAL_FRONTEND_URL; + CanonicalFrontendUrl = Program.AppConfiguration.CANONICAL_FRONTEND_URL; } public class QueryParams @@ -20,9 +20,11 @@ public class ValidateRoute : RouteBaseSync.WithRequest<ValidateRoute.QueryParams } [HttpGet("~/_/validate")] - public override ActionResult Handle([FromQuery] QueryParams request) { + public override ActionResult Handle([FromQuery] QueryParams request) + { var isFulfilled = _emailValidation.FulfillEmailValidationRequest(request.Id, LoggedInUser.Id); - if (!isFulfilled) { + if (!isFulfilled) + { _logger.LogError("Email validation fulfillment failed for request {requestId} and user {userId}", request.Id, LoggedInUser.Id); return StatusCode(400, $""" <html> diff --git a/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs index 163ddb6..c28f534 100644 --- a/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs +++ b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs @@ -5,12 +5,11 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens; public class CreateTokenRoute : RouteBaseSync.WithRequest<CreateTokenRoute.Payload>.WithActionResult { private readonly MainAppDatabase _database; - private readonly AppConfiguration _configuration; private readonly ILogger<CreateTokenRoute> _logger; - public CreateTokenRoute(MainAppDatabase database, VaultService vaultService, ILogger<CreateTokenRoute> logger) { + public CreateTokenRoute(MainAppDatabase database, ILogger<CreateTokenRoute> logger) + { _database = database; - _configuration = vaultService.GetCurrentAppConfiguration(); _logger = logger; } @@ -30,19 +29,23 @@ public class CreateTokenRoute : RouteBaseSync.WithRequest<CreateTokenRoute.Paylo /// <returns></returns> [ApiVersion(ApiSpecV1.VERSION_STRING)] [HttpPost("~/v{version:apiVersion}/api-tokens/create")] - public override ActionResult Handle(Payload request) { + public override ActionResult Handle(Payload request) + { var user = _database.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); - if (user == default) { + if (user == default) + { return NotFound(new KnownProblemModel("User does not exist")); } - var token_entropy = _configuration.APP_AES_KEY; - if (token_entropy.IsNullOrWhiteSpace()) { + var tokenEntropy = Program.AppConfiguration.APP_AES_KEY; + if (tokenEntropy.IsNullOrWhiteSpace()) + { _logger.LogWarning("No token entropy is available, Basic auth is disabled"); return NotFound(); } - var accessToken = new ApiAccessToken() { + var accessToken = new ApiAccessToken() + { User = user, ExpiryDate = request.ExpiryDate.ToUniversalTime(), AllowCreate = request.AllowCreate, @@ -53,6 +56,6 @@ public class CreateTokenRoute : RouteBaseSync.WithRequest<CreateTokenRoute.Paylo _database.AccessTokens.Add(accessToken); _database.SaveChanges(); - return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(accessToken.Id.ToString().EncryptWithAes(token_entropy)))); + return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(accessToken.Id.ToString().EncryptWithAes(tokenEntropy)))); } }
\ No newline at end of file diff --git a/code/api/src/Jobs/JobRegister.cs b/code/api/src/Jobs/JobRegister.cs index 1da7d5b..c07a6d1 100644 --- a/code/api/src/Jobs/JobRegister.cs +++ b/code/api/src/Jobs/JobRegister.cs @@ -1,3 +1,5 @@ +using Quartz.Impl; + namespace IOL.GreatOffice.Api.Jobs; public static class JobRegister @@ -7,17 +9,12 @@ public static class JobRegister public static IServiceCollectionQuartzConfigurator RegisterJobs(this IServiceCollectionQuartzConfigurator configurator) { configurator.AddJob<AccessTokenCleanupJob>(AccessTokenCleanupKey); - configurator.AddJob<VaultTokenRenewalJob>(VaultTokenRenewalKey); configurator.AddTrigger(options => { options.ForJob(AccessTokenCleanupKey) .WithIdentity(AccessTokenCleanupKey.Name + "-trigger") .WithCronSchedule("0 0 0/1 ? * * *"); }); - configurator.AddTrigger(options => { - options.ForJob(VaultTokenRenewalKey) - .WithIdentity(VaultTokenRenewalKey.Name + "-trigger") - .WithCronSchedule("0 0 0/1 ? * * *"); - }); + return configurator; } }
\ No newline at end of file diff --git a/code/api/src/Jobs/VaultTokenRenewalJob.cs b/code/api/src/Jobs/VaultTokenRenewalJob.cs deleted file mode 100644 index 1768629..0000000 --- a/code/api/src/Jobs/VaultTokenRenewalJob.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace IOL.GreatOffice.Api.Jobs; - -public class VaultTokenRenewalJob : IJob -{ - private readonly ILogger<VaultTokenRenewalJob> _logger; - private readonly VaultService _vaultService; - - public VaultTokenRenewalJob(ILogger<VaultTokenRenewalJob> logger, VaultService vaultService) { - _logger = logger; - _vaultService = vaultService; - } - - public async Task Execute(IJobExecutionContext context) { - _logger.LogInformation("Starting vault token renewal"); - var renew = await _vaultService.RenewTokenAsync(); - if (renew == default) { - _logger.LogCritical("Renewal did not succeed"); - return; - } - - var token = await _vaultService.LookupTokenAsync(); - _logger.LogInformation("Token was renewed, new expire time {expires}", token.Data.ExpireTime); - } -}
\ No newline at end of file diff --git a/code/api/src/Models/Misc/AppConfiguration.cs b/code/api/src/Models/Misc/AppConfiguration.cs index 9ed6fe4..a71970c 100644 --- a/code/api/src/Models/Misc/AppConfiguration.cs +++ b/code/api/src/Models/Misc/AppConfiguration.cs @@ -1,9 +1,42 @@ +using System.Diagnostics; using System.Security.Cryptography.X509Certificates; +using System.Text; namespace IOL.GreatOffice.Api.Models.Models; public class AppConfiguration { + public AppConfiguration() + { + + } + + public AppConfiguration(IConfiguration c) + { + DB_HOST = c.GetValue<string>(nameof(DB_HOST)); + DB_PORT = c.GetValue<string>(nameof(DB_PORT)); + DB_NAME = c.GetValue<string>(nameof(DB_NAME)); + DB_USER = c.GetValue<string>(nameof(DB_USER)); + DB_PASSWORD = c.GetValue<string>(nameof(DB_PASSWORD)); + QUARTZ_DB_HOST = c.GetValue<string>(nameof(QUARTZ_DB_HOST)); + QUARTZ_DB_NAME = c.GetValue<string>(nameof(QUARTZ_DB_NAME)); + QUARTZ_DB_PASSWORD = c.GetValue<string>(nameof(QUARTZ_DB_PASSWORD)); + QUARTZ_DB_USER = c.GetValue<string>(nameof(QUARTZ_DB_USER)); + QUARTZ_DB_PORT = c.GetValue<string>(nameof(QUARTZ_DB_PORT)); + APP_CERT = c.GetValue<string>(nameof(APP_CERT)); + APP_AES_KEY = c.GetValue<string>(nameof(APP_AES_KEY)); + SEQ_API_KEY = c.GetValue<string>(nameof(SEQ_API_KEY)); + SEQ_API_URL = c.GetValue<string>(nameof(SEQ_API_URL)); + POSTMARK_TOKEN = c.GetValue<string>(nameof(POSTMARK_TOKEN)); + EMAIL_FROM_ADDRESS = c.GetValue<string>(nameof(EMAIL_FROM_ADDRESS)); + CANONICAL_FRONTEND_URL = c.GetValue<string>(nameof(CANONICAL_FRONTEND_URL)); + CANONICAL_BACKEND_URL = c.GetValue<string>(nameof(CANONICAL_BACKEND_URL)); + ASPNETCORE_ENVIRONMENT = c.GetValue<string>(nameof(ASPNETCORE_ENVIRONMENT)); + _configuration = c; + } + + private static IConfiguration _configuration { get; set; } + /// <summary> /// An reachable ip address or url that points to a postgres database. /// </summary> @@ -94,6 +127,11 @@ public class AppConfiguration /// </summary> public string APP_CERT { get; set; } + /// <summary> + /// A string signaling to the framework what environment it is running in, usually Development, Testing or Production. + /// </summary> + public string ASPNETCORE_ENVIRONMENT { get; set; } + public X509Certificate2 CERT1() => new(Convert.FromBase64String(APP_CERT)); public object GetPublicObject() @@ -103,18 +141,68 @@ public class AppConfiguration DB_HOST, DB_PORT, DB_USER, - DB_PASSWORD = DB_PASSWORD.Obfuscate() ?? "", + DB_PASSWORD = DB_PASSWORD.Obfuscate(), QUARTZ_DB_HOST, QUARTZ_DB_PORT, QUARTZ_DB_USER, - QUARTZ_DB_PASSWORD = QUARTZ_DB_PASSWORD.Obfuscate() ?? "", - SEQ_API_KEY = SEQ_API_KEY.Obfuscate() ?? "", + QUARTZ_DB_PASSWORD = QUARTZ_DB_PASSWORD.Obfuscate(), + SEQ_API_KEY = SEQ_API_KEY.Obfuscate(), SEQ_API_URL, - POSTMARK_TOKEN = POSTMARK_TOKEN.Obfuscate() ?? "", + POSTMARK_TOKEN = POSTMARK_TOKEN.Obfuscate(), EMAIL_FROM_ADDRESS, - APP_AES_KEY = APP_AES_KEY.Obfuscate() ?? "", - CERT1 = CERT1().PublicKey.Oid.FriendlyName, - CANONICAL_FRONTEND_URL + APP_AES_KEY = APP_AES_KEY.Obfuscate(), + CERT1 = CERT1().Thumbprint, + CANONICAL_FRONTEND_URL, + ASPNETCORE_ENVIRONMENT }; } + + public string GetEnvironmentVariable(string variableName, string fallback = "") + { + if (_configuration == default) + { + Debug.WriteLine("AppConfiguration was instantiated without a full IConfiguration"); + return ""; + } + if (fallback.HasValue()) return _configuration.GetValue(variableName, fallback); + return _configuration.GetValue<string>(variableName); + } + + public string GetAppDatabaseConnectionString() + { + var builder = new StringBuilder(); + + builder.Append($"Server={DB_HOST};Port={DB_PORT};Database={DB_NAME};User Id={DB_USER};Password={DB_PASSWORD}"); + + if (ASPNETCORE_ENVIRONMENT == "Development") + { + builder.Append(";Include Error Detail=true"); + } + + Log.Debug("Using app database connection string: " + builder.ToString()); + return builder.ToString(); + } + + public string GetQuartzDatabaseConnectionString() + { + var builder = new StringBuilder(); + + builder.Append($"Server={QUARTZ_DB_HOST};Port={QUARTZ_DB_PORT};Database={QUARTZ_DB_NAME};User Id={QUARTZ_DB_USER};Password={QUARTZ_DB_PASSWORD}"); + + if (ASPNETCORE_ENVIRONMENT == "Development") + { + builder.Append(";Include Error Detail=true"); + } + + Log.Debug("Using quartz database connection string: " + builder.ToString()); + return builder.ToString(); + } + + public string GetAppVersion() + { + var versionFilePath = Path.Combine(AppPaths.AppData.HostPath, "version.txt"); + if (!File.Exists(versionFilePath)) return "unknown-" + ASPNETCORE_ENVIRONMENT; + var versionText = File.ReadAllText(versionFilePath); + return versionText + "-" + ASPNETCORE_ENVIRONMENT; + } }
\ No newline at end of file diff --git a/code/api/src/Models/Static/AppEnvironmentVariables.cs b/code/api/src/Models/Static/AppEnvironmentVariables.cs deleted file mode 100644 index c35739e..0000000 --- a/code/api/src/Models/Static/AppEnvironmentVariables.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace IOL.GreatOffice.Api.Models.Static; - -public static class AppEnvironmentVariables -{ - /// <summary> - /// An access token that can be used to access the Hashicorp Vault instance that is available at VAULT_URL - /// </summary> - public const string VAULT_TOKEN = "VAULT_TOKEN"; - - /// <summary> - /// A url pointing to the Hashicorp Vault instance the app should use - /// </summary> - public const string VAULT_URL = "VAULT_URL"; - - /// <summary> - /// The duration of which to keep a local cached version of the configuration - /// </summary> - public const string VAULT_CACHE_TTL = "VAULT_CACHE_TTL"; - - /// <summary> - /// The vault key name for the main configuration json object, described by <see cref="AppConfiguration"/> - /// </summary> - public const string MAIN_CONFIG_SHEET = "MAIN_CONFIG_SHEET"; - - /// <summary> - /// Tells the api to enable flight mode, only acts in DEBUG - /// </summary> - public const string FLIGHT_MODE = "FLIGHT_MODE"; - - /// <summary> - /// Tells the api where to read configuration from, defaults to flightmode.json, only acts in DEBUG - /// </summary> - public const string FLIGHT_MODE_JSON = "FLIGHT_MODE_JSON"; -}
\ No newline at end of file diff --git a/code/api/src/Program.cs b/code/api/src/Program.cs index 7277fd3..c0bfdf7 100644 --- a/code/api/src/Program.cs +++ b/code/api/src/Program.cs @@ -44,6 +44,7 @@ namespace IOL.GreatOffice.Api; public static class Program { private static readonly string[] supportedCultures = ["en", "nb"]; + public static AppConfiguration AppConfiguration { get; set; } public static WebApplicationBuilder CreateAppBuilder(string[] args) { var builder = WebApplication.CreateBuilder(args); @@ -55,11 +56,10 @@ public static class Program builder.Services.AddScoped<UserService>(); builder.Services.AddScoped<TenantService>(); builder.Services.AddScoped<EmailValidationService>(); - builder.Services.AddSingleton<VaultService>(); - builder.Services.AddHttpClient<VaultService>(); builder.Services.AddHttpClient<MailService>(); - var vaultService = builder.Services.BuildServiceProvider().GetRequiredService<VaultService>(); - var configuration = vaultService.GetCurrentAppConfiguration(); + + AppConfiguration = new AppConfiguration(builder.Configuration); + var logger = new LoggerConfiguration() .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", LogEventLevel.Information) @@ -68,13 +68,13 @@ public static class Program .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) .WriteTo.Console(); - if (!builder.Environment.IsDevelopment() && configuration.SEQ_API_KEY.HasValue() && configuration.SEQ_API_URL.HasValue()) + if (!builder.Environment.IsDevelopment() && AppConfiguration.SEQ_API_KEY.HasValue() && AppConfiguration.SEQ_API_URL.HasValue()) { - logger.WriteTo.Seq(configuration.SEQ_API_URL, apiKey: configuration.SEQ_API_KEY); + logger.WriteTo.Seq(AppConfiguration.SEQ_API_URL, apiKey: AppConfiguration.SEQ_API_KEY); } Log.Logger = logger.CreateLogger(); - Log.Information("Starting web host, " + JsonSerializer.Serialize(configuration.GetPublicObject(), JsonSettings.WriteIndented)); + Log.Information("Starting web host, " + JsonSerializer.Serialize(AppConfiguration.GetPublicObject(), JsonSettings.WriteIndented)); builder.Host.UseSerilog(Log.Logger); @@ -107,18 +107,18 @@ public static class Program builder.Services .AddDataProtection() - .ProtectKeysWithCertificate(configuration.CERT1()) + .ProtectKeysWithCertificate(AppConfiguration.CERT1()) .PersistKeysToDbContext<MainAppDatabase>(); builder.Services.Configure(JsonSettings.SetDefaultAction); - builder.Services.AddQuartz(options => { options.UsePersistentStore(o => { - o.UsePostgres(builder.Configuration.GetQuartzDatabaseConnectionString(vaultService.GetCurrentAppConfiguration)); + o.UsePostgres(AppConfiguration.GetQuartzDatabaseConnectionString()); o.UseSerializer<QuartzJsonSerializer>(); }); + options.RegisterJobs(); }); @@ -146,7 +146,7 @@ public static class Program builder.Services.AddDbContext<MainAppDatabase>(options => { - options.UseNpgsql(builder.Configuration.GetAppDatabaseConnectionString(vaultService.GetCurrentAppConfiguration), + options.UseNpgsql(AppConfiguration.GetAppDatabaseConnectionString(), npgsqlDbContextOptionsBuilder => { npgsqlDbContextOptionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); diff --git a/code/api/src/Services/EmailValidationService.cs b/code/api/src/Services/EmailValidationService.cs index c7be20a..5d909f3 100644 --- a/code/api/src/Services/EmailValidationService.cs +++ b/code/api/src/Services/EmailValidationService.cs @@ -8,29 +8,33 @@ public class EmailValidationService private readonly ILogger<EmailValidationService> _logger; private readonly string EmailValidationUrl; - public EmailValidationService(IStringLocalizer<SharedResources> localizer, MainAppDatabase database, MailService mailService, ILogger<EmailValidationService> logger, VaultService vaultService) { + public EmailValidationService(IStringLocalizer<SharedResources> localizer, MainAppDatabase database, MailService mailService, ILogger<EmailValidationService> logger) + { _localizer = localizer; _database = database; _mailService = mailService; _logger = logger; - var configuration = vaultService.GetCurrentAppConfiguration(); - EmailValidationUrl = configuration.CANONICAL_BACKEND_URL + "/_/validate"; + EmailValidationUrl = Program.AppConfiguration.CANONICAL_BACKEND_URL + "/_/validate"; } - public bool FulfillEmailValidationRequest(Guid id, Guid userId) { + public bool FulfillEmailValidationRequest(Guid id, Guid userId) + { var item = _database.ValidationEmails.FirstOrDefault(c => c.Id == id); - if (item == default) { + if (item == default) + { _logger.LogDebug("Did not find email validation request with id: {requestId}", id); return false; } - if (item.UserId != userId) { + 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) { + if (user == default) + { _database.ValidationEmails.Remove(item); _database.SaveChanges(); _logger.LogInformation("Deleting request {requestId} because user does not exist anymore", id); @@ -45,12 +49,15 @@ public class EmailValidationService return true; } - public async Task SendValidationEmailAsync(User user) { - var queueItem = new ValidationEmail() { + public async Task SendValidationEmailAsync(User user) + { + var queueItem = new ValidationEmail() + { UserId = user.Id, Id = Guid.NewGuid() }; - var email = new MailService.PostmarkEmail() { + var email = new MailService.PostmarkEmail() + { To = user.Username, Subject = _localizer["Greatoffice Email Validation"], TextBody = _localizer[""" diff --git a/code/api/src/Services/MailService.cs b/code/api/src/Services/MailService.cs index a6f7db4..4cc5288 100644 --- a/code/api/src/Services/MailService.cs +++ b/code/api/src/Services/MailService.cs @@ -6,11 +6,11 @@ public class MailService private static string _fromEmail; private readonly HttpClient _httpClient; - public MailService(VaultService vaultService, ILogger<MailService> logger, HttpClient httpClient) { - var configuration = vaultService.GetCurrentAppConfiguration(); - _fromEmail = configuration.EMAIL_FROM_ADDRESS; + public MailService(ILogger<MailService> logger, HttpClient httpClient) + { + _fromEmail = Program.AppConfiguration.EMAIL_FROM_ADDRESS; _logger = logger; - httpClient.DefaultRequestHeaders.Add("X-Postmark-Server-Token", configuration.POSTMARK_TOKEN); + httpClient.DefaultRequestHeaders.Add("X-Postmark-Server-Token", Program.AppConfiguration.POSTMARK_TOKEN); _httpClient = httpClient; } @@ -19,35 +19,46 @@ public class MailService /// </summary> /// <param name="message"></param> /// <exception cref="ArgumentException"></exception> - public async Task SendMailAsync(PostmarkEmail message) { - try { - if (message.MessageStream.IsNullOrWhiteSpace()) { + public async Task SendMailAsync(PostmarkEmail message) + { + try + { + if (message.MessageStream.IsNullOrWhiteSpace()) + { message.MessageStream = "outbound"; } - if (message.From.IsNullOrWhiteSpace() && _fromEmail.HasValue()) { + if (message.From.IsNullOrWhiteSpace() && _fromEmail.HasValue()) + { message.From = _fromEmail; - } else { + } + else + { throw new ApplicationException("Not one from-email is available"); } - if (message.To.IsNullOrWhiteSpace()) { + if (message.To.IsNullOrWhiteSpace()) + { throw new ArgumentNullException(nameof(message.To), "A recipient should be specified."); } - if (!message.To.IsValidEmailAddress()) { + if (!message.To.IsValidEmailAddress()) + { throw new ArgumentException(nameof(message.To), "To is not a valid email address"); } - if (message.HtmlBody.IsNullOrWhiteSpace() && message.TextBody.IsNullOrWhiteSpace()) { + if (message.HtmlBody.IsNullOrWhiteSpace() && message.TextBody.IsNullOrWhiteSpace()) + { throw new ArgumentNullException(nameof(message), "Both HtmlBody and TextBody is empty, nothing to send"); } #if DEBUG - _logger.LogInformation("Sending email: {0}", JsonSerializer.Serialize(message, new JsonSerializerOptions() {WriteIndented = true})); + _logger.LogInformation("Sending email: {0}", JsonSerializer.Serialize(message, new JsonSerializerOptions() { WriteIndented = true })); #endif 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) { + } + catch (Exception e) + { _logger.LogError(e, "A silent exception occured while trying to send an email"); } } @@ -60,7 +71,7 @@ public class MailService public Guid MessageID { get; set; } /// <summary> - /// The message from the API. + /// The message from the API. /// In the event of an error, this message may contain helpful text. /// </summary> public string Message { get; set; } diff --git a/code/api/src/Services/PasswordResetService.cs b/code/api/src/Services/PasswordResetService.cs index d4aeb0d..7f70ad1 100644 --- a/code/api/src/Services/PasswordResetService.cs +++ b/code/api/src/Services/PasswordResetService.cs @@ -4,18 +4,15 @@ public class PasswordResetService { private readonly MainAppDatabase _database; private readonly MailService _mailService; - private readonly AppConfiguration _configuration; private readonly ILogger<PasswordResetService> _logger; private readonly IStringLocalizer<SharedResources> _localizer; public PasswordResetService( MainAppDatabase database, - VaultService vaultService, ILogger<PasswordResetService> logger, MailService mailService, IStringLocalizer<SharedResources> localizer) { _database = database; - _configuration = vaultService.GetCurrentAppConfiguration(); _logger = logger; _mailService = mailService; _localizer = localizer; @@ -69,7 +66,7 @@ Go to the following link to set a new password. The link expires at {3}. If you did not request a password reset, no action is required. -""", user.DisplayName(true), _configuration.CANONICAL_FRONTEND_URL, request.Id, zonedExpirationDate.ToString("yyyy-MM-dd hh:mm")] +""", user.DisplayName(true), Program.AppConfiguration.CANONICAL_FRONTEND_URL, request.Id, zonedExpirationDate.ToString("yyyy-MM-dd hh:mm")] }; await Task.Run(() => diff --git a/code/api/src/Services/VaultService.cs b/code/api/src/Services/VaultService.cs deleted file mode 100644 index 3290e30..0000000 --- a/code/api/src/Services/VaultService.cs +++ /dev/null @@ -1,219 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; - -namespace IOL.GreatOffice.Api.Services; - -public class VaultService -{ - private readonly HttpClient _client; - private readonly IMemoryCache _cache; - private readonly IConfiguration _configuration; - private int CACHE_TTL { get; set; } - - 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); - if (token.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_TOKEN is empty"); - if (vaultUrl.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_URL is empty"); - client.DefaultRequestHeaders.Add("X-Vault-Token", token); - client.BaseAddress = new Uri(vaultUrl); - _client = client; - _cache = cache; - _configuration = configuration; - } - - public async Task<T> GetSecretJsonAsync<T>(string path) { - var result = await _cache.GetOrCreate(AppConstants.VAULT_CACHE_KEY, - async cacheEntry => { - cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(CACHE_TTL); - var getSecretResponse = await _client.GetFromJsonAsync<GetSecretResponse<T>>("/v1/kv/data/" + path); - if (getSecretResponse == null) { - return default; - } - - Log.Debug("Setting new vault cache, " + new { - PATH = path, - CACHE_TTL, - Data = JsonSerializer.Serialize(getSecretResponse.Data.Data) - }); - return getSecretResponse.Data.Data ?? default; - }); - return result; - } - - public Task<T> RefreshAsync<T>(string path) { - _cache.Remove(AppConstants.VAULT_CACHE_KEY); - CACHE_TTL = _configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12); - return GetSecretJsonAsync<T>(path); - } - - public async Task<RenewTokenResponse> RenewTokenAsync() { - var response = await _client.PostAsJsonAsync("v1/auth/token/renew-self", new {increment = "2h"}); - if (response.IsSuccessStatusCode) { - return await response.Content.ReadFromJsonAsync<RenewTokenResponse>(); - } - - return default; - } - - public async Task<TokenLookupResponse> LookupTokenAsync() { - var response = await _client.GetAsync("v1/auth/token/lookup-self"); - if (response.IsSuccessStatusCode) { - return await response.Content.ReadFromJsonAsync<TokenLookupResponse>(); - } - - return default; - } - - public AppConfiguration GetCurrentAppConfiguration() { -#if DEBUG - if (_configuration.GetValue<bool>(AppEnvironmentVariables.FLIGHT_MODE)) { - var text = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), _configuration.GetValue(AppEnvironmentVariables.FLIGHT_MODE_JSON, "flightmode.json"))); - return JsonSerializer.Deserialize<AppConfiguration>(text); - } -#endif - - return GetSecretJsonAsync<AppConfiguration>( - _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET) - ).Result; - } - - public async Task RefreshCurrentAppConfigurationAsync() { - await RefreshAsync<AppConfiguration>(_configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET)); - } - - public class RenewTokenResponse - { - public Guid RequestId { get; set; } - public string LeaseId { get; set; } - public bool Renewable { get; set; } - public long LeaseDuration { get; set; } - public object Data { get; set; } - public object WrapInfo { get; set; } - public List<string> Warnings { get; set; } - public Auth Auth { get; set; } - } - - public class Auth - { - public string ClientToken { get; set; } - public string Accessor { get; set; } - public List<string> Policies { get; set; } - public List<string> TokenPolicies { get; set; } - public object Metadata { get; set; } - public long LeaseDuration { get; set; } - public bool Renewable { get; set; } - public string EntityId { get; set; } - public string TokenType { get; set; } - public bool Orphan { get; set; } - public object MfaRequirement { get; set; } - public long NumUses { get; set; } - } - - public class GetSecretResponse<T> - { - public VaultSecret<T> Data { get; set; } - } - - public class VaultSecret<T> - { - public T Data { get; set; } - public VaultSecretMetadata Metadata { get; set; } - } - - public class VaultSecretMetadata - { - public DateTimeOffset CreatedTime { get; set; } - public object CustomMetadata { get; set; } - public string DeletionTime { get; set; } - public bool Destroyed { get; set; } - public long Version { get; set; } - } - - public class TokenLookupResponse - { - [JsonPropertyName("request_id")] - public Guid RequestId { get; set; } - - [JsonPropertyName("lease_id")] - public string LeaseId { get; set; } - - [JsonPropertyName("renewable")] - public bool Renewable { get; set; } - - [JsonPropertyName("lease_duration")] - public long LeaseDuration { get; set; } - - [JsonPropertyName("data")] - public TokenLookupResponseData Data { get; set; } - - [JsonPropertyName("wrap_info")] - public object WrapInfo { get; set; } - - [JsonPropertyName("warnings")] - public object Warnings { get; set; } - - [JsonPropertyName("auth")] - public object Auth { get; set; } - } - - public class TokenLookupResponseData - { - [JsonPropertyName("accessor")] - public string Accessor { get; set; } - - [JsonPropertyName("creation_time")] - public long CreationTime { get; set; } - - [JsonPropertyName("creation_ttl")] - public long CreationTtl { get; set; } - - [JsonPropertyName("display_name")] - public string DisplayName { get; set; } - - [JsonPropertyName("entity_id")] - public string EntityId { get; set; } - - [JsonPropertyName("expire_time")] - public DateTimeOffset ExpireTime { get; set; } - - [JsonPropertyName("explicit_max_ttl")] - public long ExplicitMaxTtl { get; set; } - - [JsonPropertyName("id")] - public string Id { get; set; } - - [JsonPropertyName("issue_time")] - public DateTimeOffset IssueTime { get; set; } - - [JsonPropertyName("last_renewal")] - public DateTimeOffset LastRenewal { get; set; } - - [JsonPropertyName("last_renewal_time")] - public long LastRenewalTime { get; set; } - - [JsonPropertyName("meta")] - public object Meta { get; set; } - - [JsonPropertyName("num_uses")] - public long NumUses { get; set; } - - [JsonPropertyName("orphan")] - public bool Orphan { get; set; } - - [JsonPropertyName("path")] - public string Path { get; set; } - - [JsonPropertyName("policies")] - public List<string> Policies { get; set; } - - [JsonPropertyName("renewable")] - public bool Renewable { get; set; } - - [JsonPropertyName("ttl")] - public long Ttl { get; set; } - - [JsonPropertyName("type")] - public string Type { get; set; } - } -}
\ No newline at end of file diff --git a/code/api/src/Utilities/BasicAuthenticationHandler.cs b/code/api/src/Utilities/BasicAuthenticationHandler.cs index 41486ef..52ba75a 100644 --- a/code/api/src/Utilities/BasicAuthenticationHandler.cs +++ b/code/api/src/Utilities/BasicAuthenticationHandler.cs @@ -8,20 +8,17 @@ namespace IOL.GreatOffice.Api.Utilities; public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { private readonly MainAppDatabase _context; - private readonly AppConfiguration _configuration; private readonly ILogger<BasicAuthenticationHandler> _logger; public BasicAuthenticationHandler( IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, - MainAppDatabase context, - VaultService vaultService + MainAppDatabase context ) : base(options, logger, encoder) { _context = context; - _configuration = vaultService.GetCurrentAppConfiguration(); _logger = logger.CreateLogger<BasicAuthenticationHandler>(); } @@ -36,7 +33,7 @@ public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSc try { - var tokenEntropy = _configuration.APP_AES_KEY; + var tokenEntropy = Program.AppConfiguration.APP_AES_KEY; if (tokenEntropy.IsNullOrWhiteSpace()) { _logger.LogWarning("No token entropy is available in env:TOKEN_ENTROPY, Basic auth is disabled"); diff --git a/code/api/src/Utilities/ConfigurationExtensions.cs b/code/api/src/Utilities/ConfigurationExtensions.cs deleted file mode 100644 index c95e293..0000000 --- a/code/api/src/Utilities/ConfigurationExtensions.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace IOL.GreatOffice.Api.Utilities; - -public static class ConfigurationExtensions -{ - public static string GetAppDatabaseConnectionString(this IConfiguration config, AppConfiguration configuration) { - var host = configuration.DB_HOST; - var port = configuration.DB_PORT; - var database = configuration.DB_NAME; - var user = configuration.DB_USER; - var password = configuration.DB_PASSWORD; - - string result; - if (config.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Development") { - result = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true"; - } else { - result = $"Server={host};Port={port};Database={database};User Id={user};Password={password}"; - } - - Log.Debug("Using app database connection string: " + result); - return result; - } - - public static string GetAppDatabaseConnectionString(this IConfiguration config, Func<AppConfiguration> configuration) { - var _configuration = configuration(); - var host = _configuration.DB_HOST; - var port = _configuration.DB_PORT; - var database = _configuration.DB_NAME; - var user = _configuration.DB_USER; - var password = _configuration.DB_PASSWORD; - - string result; - if (config.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Development") { - result = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true"; - } else { - result = $"Server={host};Port={port};Database={database};User Id={user};Password={password}"; - } - - Log.Debug("Using app database connection string: " + result); - return result; - } - - public static string GetQuartzDatabaseConnectionString(this IConfiguration config, AppConfiguration configuration) { - var host = configuration.QUARTZ_DB_HOST; - var port = configuration.QUARTZ_DB_PORT; - var database = configuration.QUARTZ_DB_NAME; - var user = configuration.QUARTZ_DB_USER; - var password = configuration.QUARTZ_DB_PASSWORD; - - string result; - if (config.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Development") { - result = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true"; - } else { - result = $"Server={host};Port={port};Database={database};User Id={user};Password={password}"; - } - - Log.Debug("Using quartz database connection string: " + result); - return result; - } - - public static string GetQuartzDatabaseConnectionString(this IConfiguration config, Func<AppConfiguration> configuration) { - var _configuration = configuration(); - var host = _configuration.QUARTZ_DB_HOST; - var port = _configuration.QUARTZ_DB_PORT; - var database = _configuration.QUARTZ_DB_NAME; - var user = _configuration.QUARTZ_DB_USER; - var password = _configuration.QUARTZ_DB_PASSWORD; - - string result; - if (config.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Development") { - result = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true"; - } else { - result = $"Server={host};Port={port};Database={database};User Id={user};Password={password}"; - } - - Log.Debug("Using quartz database connection string: " + result); - return result; - } - - public static string GetVersion(this IConfiguration configuration) { - var versionFilePath = Path.Combine(AppPaths.AppData.HostPath, "version.txt"); - if (!File.Exists(versionFilePath)) return "unknown-" + configuration.GetValue<string>("ASPNETCORE_ENVIRONMENT"); - var versionText = File.ReadAllText(versionFilePath); - return versionText + "-" + configuration.GetValue<string>("ASPNETCORE_ENVIRONMENT"); - } -} |
