diff --git a/web/backend/API/API.Core/API.Core.csproj b/web/backend/API/API.Core/API.Core.csproj
index f53cbdcd..953b4f3a 100644
--- a/web/backend/API/API.Core/API.Core.csproj
+++ b/web/backend/API/API.Core/API.Core.csproj
@@ -18,6 +18,7 @@
+
diff --git a/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs b/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs
index e0de10d6..35915fe1 100644
--- a/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs
+++ b/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs
@@ -1,5 +1,6 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
+using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
@@ -32,9 +33,9 @@ public class JwtAuthenticationHandler(
protected override async Task HandleAuthenticateAsync()
{
// Use the same access-token secret source as TokenService to avoid mismatched validation.
- string? secret = configuration["ACCESS_TOKEN_SECRET"];
+ string? secret = configuration[ConfigurationKeys.AccessTokenSecret];
if (string.IsNullOrWhiteSpace(secret))
- secret = configuration["Jwt:SecretKey"];
+ secret = configuration[ConfigurationKeys.JwtSecretKeyFallback];
if (string.IsNullOrWhiteSpace(secret))
return AuthenticateResult.Fail("JWT secret is not configured");
diff --git a/web/backend/API/API.Specs/API.Specs.csproj b/web/backend/API/API.Specs/API.Specs.csproj
index 3dffa56b..2cd5bccc 100644
--- a/web/backend/API/API.Specs/API.Specs.csproj
+++ b/web/backend/API/API.Specs/API.Specs.csproj
@@ -38,6 +38,7 @@
+
diff --git a/web/backend/API/API.Specs/Steps/AuthSteps.cs b/web/backend/API/API.Specs/Steps/AuthSteps.cs
index a49f1308..9b1499ec 100644
--- a/web/backend/API/API.Specs/Steps/AuthSteps.cs
+++ b/web/backend/API/API.Specs/Steps/AuthSteps.cs
@@ -1,6 +1,7 @@
using System.Text;
using System.Text.Json;
using FluentAssertions;
+using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -430,7 +431,7 @@ public class AuthSteps(ScenarioContext scenario)
? user
: throw new InvalidOperationException("registered username not found in scenario");
- string secret = GetRequiredConfigValue("ACCESS_TOKEN_SECRET");
+ string secret = GetRequiredConfigValue(ConfigurationKeys.AccessTokenSecret);
scenario["accessToken"] = GenerateJwtToken(
userId,
username,
@@ -449,7 +450,7 @@ public class AuthSteps(ScenarioContext scenario)
? user
: throw new InvalidOperationException("registered username not found in scenario");
- string secret = GetRequiredConfigValue("CONFIRMATION_TOKEN_SECRET");
+ string secret = GetRequiredConfigValue(ConfigurationKeys.ConfirmationTokenSecret);
scenario["confirmationToken"] = GenerateJwtToken(
userId,
username,
@@ -468,7 +469,7 @@ public class AuthSteps(ScenarioContext scenario)
? user
: throw new InvalidOperationException("registered username not found in scenario");
- string secret = GetRequiredConfigValue("CONFIRMATION_TOKEN_SECRET");
+ string secret = GetRequiredConfigValue(ConfigurationKeys.ConfirmationTokenSecret);
scenario["confirmationToken"] = GenerateJwtToken(
userId,
username,
diff --git a/web/backend/Database/Database.Connection/ConfigurationHelpers.cs b/web/backend/Database/Database.Connection/ConfigurationHelpers.cs
deleted file mode 100644
index 73340cf0..00000000
--- a/web/backend/Database/Database.Connection/ConfigurationHelpers.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Microsoft.Extensions.Configuration;
-
-namespace Database.Connection;
-
-public static class ConfigurationHelpers
-{
- public static string GetKeyOrThrow(IConfiguration configuration, string key) =>
- configuration[key]
- ?? throw new InvalidOperationException($"The {key} environment variable is not set.");
-}
diff --git a/web/backend/Database/Database.Connection/Database.Connection.csproj b/web/backend/Database/Database.Connection/Database.Connection.csproj
index 0cebb12a..ef78601b 100644
--- a/web/backend/Database/Database.Connection/Database.Connection.csproj
+++ b/web/backend/Database/Database.Connection/Database.Connection.csproj
@@ -11,11 +11,14 @@
+
+
+
diff --git a/web/backend/Database/Database.Connection/DefaultSqlConnectionFactory.cs b/web/backend/Database/Database.Connection/DefaultSqlConnectionFactory.cs
index c2b3996b..97e7f588 100644
--- a/web/backend/Database/Database.Connection/DefaultSqlConnectionFactory.cs
+++ b/web/backend/Database/Database.Connection/DefaultSqlConnectionFactory.cs
@@ -1,4 +1,4 @@
-using System.Data.Common;
+using Infrastructure.Configuration;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
@@ -19,7 +19,7 @@ public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlCon
{
get
{
- string? fullConnectionString = configuration["DB_CONNECTION_STRING"];
+ string? fullConnectionString = configuration[ConfigurationKeys.DbConnectionString];
if (!string.IsNullOrEmpty(fullConnectionString))
return fullConnectionString;
diff --git a/web/backend/Database/Database.Connection/SqlConnectionStringHelper.cs b/web/backend/Database/Database.Connection/SqlConnectionStringHelper.cs
index 97e1fba3..3c88788d 100644
--- a/web/backend/Database/Database.Connection/SqlConnectionStringHelper.cs
+++ b/web/backend/Database/Database.Connection/SqlConnectionStringHelper.cs
@@ -1,3 +1,4 @@
+using Infrastructure.Configuration;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
@@ -14,9 +15,15 @@ public static class SqlConnectionStringHelper
) =>
new()
{
- DataSource = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_SERVER"),
- UserID = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_USER"),
- Password = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_PASSWORD"),
+ DataSource = ConfigurationHelpers.GetKeyOrThrow(
+ configuration,
+ ConfigurationKeys.DbServer
+ ),
+ UserID = ConfigurationHelpers.GetKeyOrThrow(configuration, ConfigurationKeys.DbUser),
+ Password = ConfigurationHelpers.GetKeyOrThrow(
+ configuration,
+ ConfigurationKeys.DbPassword
+ ),
TrustServerCertificate = ResolveTrustServerCertificate(configuration),
Encrypt = true,
};
@@ -38,7 +45,10 @@ public static class SqlConnectionStringHelper
try
{
SqlConnectionStringBuilder builder = MakeConfiguredConnectionBuilder(configuration);
- builder.InitialCatalog = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_NAME");
+ builder.InitialCatalog = ConfigurationHelpers.GetKeyOrThrow(
+ configuration,
+ ConfigurationKeys.DbName
+ );
connectionString = builder.ConnectionString;
return true;
}
@@ -95,7 +105,7 @@ public static class SqlConnectionStringHelper
private static bool ResolveTrustServerCertificate(IConfiguration configuration)
{
return !bool.TryParse(
- configuration["DB_TRUST_SERVER_CERTIFICATE"],
+ configuration[ConfigurationKeys.DbTrustServerCertificate],
out bool trustServerCertificate
) || trustServerCertificate;
}
diff --git a/web/backend/Database/Database.Migrations/Database.Migrations.csproj b/web/backend/Database/Database.Migrations/Database.Migrations.csproj
index 42038c47..1e26fcd7 100644
--- a/web/backend/Database/Database.Migrations/Database.Migrations.csproj
+++ b/web/backend/Database/Database.Migrations/Database.Migrations.csproj
@@ -19,6 +19,7 @@
+
diff --git a/web/backend/Database/Database.Migrations/Program.cs b/web/backend/Database/Database.Migrations/Program.cs
index 7ad60ecb..cbded8f0 100644
--- a/web/backend/Database/Database.Migrations/Program.cs
+++ b/web/backend/Database/Database.Migrations/Program.cs
@@ -1,4 +1,5 @@
using Database.Connection;
+using Infrastructure.Configuration;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
@@ -21,7 +22,7 @@ internal static class Program
.Build();
bool clearDatabase = string.Equals(
- configuration["CLEAR_DATABASE"],
+ configuration[ConfigurationKeys.ClearDatabase],
"true",
StringComparison.OrdinalIgnoreCase
);
diff --git a/web/backend/Features/Features.Auth.Tests/Features.Auth.Tests.csproj b/web/backend/Features/Features.Auth.Tests/Features.Auth.Tests.csproj
index 7c743e53..aa3c8bff 100644
--- a/web/backend/Features/Features.Auth.Tests/Features.Auth.Tests.csproj
+++ b/web/backend/Features/Features.Auth.Tests/Features.Auth.Tests.csproj
@@ -21,5 +21,6 @@
+
diff --git a/web/backend/Features/Features.Auth.Tests/Services/TokenServiceRefreshTests.cs b/web/backend/Features/Features.Auth.Tests/Services/TokenServiceRefreshTests.cs
index b78bd5ae..2c8c3f0b 100644
--- a/web/backend/Features/Features.Auth.Tests/Services/TokenServiceRefreshTests.cs
+++ b/web/backend/Features/Features.Auth.Tests/Services/TokenServiceRefreshTests.cs
@@ -5,6 +5,7 @@ using Features.Auth.Identity;
using Features.Auth.Services;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
+using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
@@ -27,9 +28,11 @@ public class TokenServiceRefreshTests
.AddInMemoryCollection(
new Dictionary
{
- ["ACCESS_TOKEN_SECRET"] = "test-access-secret-that-is-very-long-1234567890",
- ["REFRESH_TOKEN_SECRET"] = "test-refresh-secret-that-is-very-long-1234567890",
- ["CONFIRMATION_TOKEN_SECRET"] =
+ [ConfigurationKeys.AccessTokenSecret] =
+ "test-access-secret-that-is-very-long-1234567890",
+ [ConfigurationKeys.RefreshTokenSecret] =
+ "test-refresh-secret-that-is-very-long-1234567890",
+ [ConfigurationKeys.ConfirmationTokenSecret] =
"test-confirmation-secret-that-is-very-long-1234567890",
}
)
diff --git a/web/backend/Features/Features.Auth.Tests/Services/TokenServiceValidationTests.cs b/web/backend/Features/Features.Auth.Tests/Services/TokenServiceValidationTests.cs
index 0c06683b..df73095c 100644
--- a/web/backend/Features/Features.Auth.Tests/Services/TokenServiceValidationTests.cs
+++ b/web/backend/Features/Features.Auth.Tests/Services/TokenServiceValidationTests.cs
@@ -4,6 +4,7 @@ using Domain.Exceptions;
using Features.Auth.Services;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
+using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.Extensions.Configuration;
using Moq;
@@ -23,9 +24,11 @@ public class TokenServiceValidationTests
.AddInMemoryCollection(
new Dictionary
{
- ["ACCESS_TOKEN_SECRET"] = "test-access-secret-that-is-very-long-1234567890",
- ["REFRESH_TOKEN_SECRET"] = "test-refresh-secret-that-is-very-long-1234567890",
- ["CONFIRMATION_TOKEN_SECRET"] =
+ [ConfigurationKeys.AccessTokenSecret] =
+ "test-access-secret-that-is-very-long-1234567890",
+ [ConfigurationKeys.RefreshTokenSecret] =
+ "test-refresh-secret-that-is-very-long-1234567890",
+ [ConfigurationKeys.ConfirmationTokenSecret] =
"test-confirmation-secret-that-is-very-long-1234567890",
}
)
diff --git a/web/backend/Features/Features.Auth/Features.Auth.csproj b/web/backend/Features/Features.Auth/Features.Auth.csproj
index 1ea6af4c..53379c51 100644
--- a/web/backend/Features/Features.Auth/Features.Auth.csproj
+++ b/web/backend/Features/Features.Auth/Features.Auth.csproj
@@ -14,6 +14,7 @@
+
diff --git a/web/backend/Features/Features.Auth/Services/TokenService.cs b/web/backend/Features/Features.Auth/Services/TokenService.cs
index 00f3e4ce..d9ebe6d8 100644
--- a/web/backend/Features/Features.Auth/Services/TokenService.cs
+++ b/web/backend/Features/Features.Auth/Services/TokenService.cs
@@ -2,6 +2,7 @@ using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Domain.Exceptions;
using Features.Auth.Identity;
+using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
@@ -33,23 +34,20 @@ public class TokenService : ITokenService
_tokenInfrastructure = tokenInfrastructure;
_userManager = userManager;
- _accessTokenSecret =
- configuration["ACCESS_TOKEN_SECRET"]
- ?? throw new InvalidOperationException(
- "ACCESS_TOKEN_SECRET environment variable is not set"
- );
+ _accessTokenSecret = ConfigurationHelpers.GetKeyOrThrow(
+ configuration,
+ ConfigurationKeys.AccessTokenSecret
+ );
- _refreshTokenSecret =
- configuration["REFRESH_TOKEN_SECRET"]
- ?? throw new InvalidOperationException(
- "REFRESH_TOKEN_SECRET environment variable is not set"
- );
+ _refreshTokenSecret = ConfigurationHelpers.GetKeyOrThrow(
+ configuration,
+ ConfigurationKeys.RefreshTokenSecret
+ );
- _confirmationTokenSecret =
- configuration["CONFIRMATION_TOKEN_SECRET"]
- ?? throw new InvalidOperationException(
- "CONFIRMATION_TOKEN_SECRET environment variable is not set"
- );
+ _confirmationTokenSecret = ConfigurationHelpers.GetKeyOrThrow(
+ configuration,
+ ConfigurationKeys.ConfirmationTokenSecret
+ );
}
///
diff --git a/web/backend/Features/Features.Emails/Features.Emails.csproj b/web/backend/Features/Features.Emails/Features.Emails.csproj
index 9271696c..713ce39d 100644
--- a/web/backend/Features/Features.Emails/Features.Emails.csproj
+++ b/web/backend/Features/Features.Emails/Features.Emails.csproj
@@ -14,6 +14,7 @@
+
diff --git a/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs b/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs
index 32395220..2fb89f19 100644
--- a/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs
+++ b/web/backend/Features/Features.Emails/Services/EmailDispatcher.cs
@@ -1,3 +1,4 @@
+using Infrastructure.Configuration;
using Infrastructure.Email;
using Infrastructure.Email.Templates.Rendering;
using Microsoft.Extensions.Configuration;
@@ -21,8 +22,7 @@ public class EmailDispatcher(
/// Thrown when the WEBSITE_BASE_URL configuration value is not set.
///
private string WebsiteBaseUrl =>
- configuration["WEBSITE_BASE_URL"]
- ?? throw new InvalidOperationException("WEBSITE_BASE_URL environment variable is not set");
+ ConfigurationHelpers.GetKeyOrThrow(configuration, ConfigurationKeys.WebsiteBaseUrl);
///
public async Task SendRegistrationEmailAsync(
diff --git a/web/backend/Infrastructure/Infrastructure.Configuration/ConfigurationHelpers.cs b/web/backend/Infrastructure/Infrastructure.Configuration/ConfigurationHelpers.cs
new file mode 100644
index 00000000..4238565f
--- /dev/null
+++ b/web/backend/Infrastructure/Infrastructure.Configuration/ConfigurationHelpers.cs
@@ -0,0 +1,32 @@
+using Microsoft.Extensions.Configuration;
+
+namespace Infrastructure.Configuration;
+
+public static class ConfigurationHelpers
+{
+ public static string GetKeyOrThrow(IConfiguration configuration, string key) =>
+ configuration[key]
+ ?? throw new InvalidOperationException($"The {key} environment variable is not set.");
+
+ public static bool GetBooleanKeyOrThrow(IConfiguration configuration, string key)
+ {
+ string value = GetKeyOrThrow(configuration, key);
+
+ return bool.TryParse(value, out bool result)
+ ? result
+ : throw new InvalidOperationException(
+ $"The {key} environment variable is set to '{value}', which is not a valid boolean."
+ );
+ }
+
+ public static int GetIntKeyOrThrow(IConfiguration configuration, string key)
+ {
+ string value = GetKeyOrThrow(configuration, key);
+
+ return int.TryParse(value, out int result)
+ ? result
+ : throw new InvalidOperationException(
+ $"The {key} environment variable is set to '{value}', which is not a valid integer."
+ );
+ }
+}
diff --git a/web/backend/Infrastructure/Infrastructure.Configuration/ConfigurationKeys.cs b/web/backend/Infrastructure/Infrastructure.Configuration/ConfigurationKeys.cs
new file mode 100644
index 00000000..0238734e
--- /dev/null
+++ b/web/backend/Infrastructure/Infrastructure.Configuration/ConfigurationKeys.cs
@@ -0,0 +1,33 @@
+namespace Infrastructure.Configuration;
+
+public static class ConfigurationKeys
+{
+ public const string AccessTokenSecret = "ACCESS_TOKEN_SECRET";
+ public const string RefreshTokenSecret = "REFRESH_TOKEN_SECRET";
+ public const string ConfirmationTokenSecret = "CONFIRMATION_TOKEN_SECRET";
+
+ ///
+ /// Fallback configuration section path for the access-token signing key, checked when
+ /// is not set.
+ ///
+ public const string JwtSecretKeyFallback = "Jwt:SecretKey";
+
+ public const string WebsiteBaseUrl = "WEBSITE_BASE_URL";
+
+ public const string SmtpHost = "SMTP_HOST";
+ public const string SmtpPort = "SMTP_PORT";
+ public const string SmtpUsername = "SMTP_USERNAME";
+ public const string SmtpPassword = "SMTP_PASSWORD";
+ public const string SmtpUseSsl = "SMTP_USE_SSL";
+ public const string SmtpFromEmail = "SMTP_FROM_EMAIL";
+ public const string SmtpFromName = "SMTP_FROM_NAME";
+
+ public const string DbServer = "DB_SERVER";
+ public const string DbUser = "DB_USER";
+ public const string DbPassword = "DB_PASSWORD";
+ public const string DbName = "DB_NAME";
+ public const string DbConnectionString = "DB_CONNECTION_STRING";
+ public const string DbTrustServerCertificate = "DB_TRUST_SERVER_CERTIFICATE";
+
+ public const string ClearDatabase = "CLEAR_DATABASE";
+}
diff --git a/web/backend/Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj b/web/backend/Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj
new file mode 100644
index 00000000..a4fbf1bb
--- /dev/null
+++ b/web/backend/Infrastructure/Infrastructure.Configuration/Infrastructure.Configuration.csproj
@@ -0,0 +1,15 @@
+
+
+ net10.0
+ enable
+ enable
+ Infrastructure.Configuration
+
+
+
+
+
+
diff --git a/web/backend/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj b/web/backend/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj
index 24c165b5..7869192a 100644
--- a/web/backend/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj
+++ b/web/backend/Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj
@@ -13,4 +13,8 @@
Version="10.0.1"
/>
+
+
+
+
diff --git a/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs b/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs
index 724eb2d4..2c97a641 100644
--- a/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs
+++ b/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs
@@ -1,4 +1,5 @@
-using MailKit.Net.Smtp;
+using Infrastructure.Configuration;
+using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Configuration;
using MimeKit;
@@ -25,31 +26,27 @@ public class SmtpEmailProvider : IEmailProvider
/// SMTP_FROM_EMAIL, SMTP_FROM_NAME) from .
///
///
- /// Thrown when SMTP_HOST or SMTP_FROM_EMAIL is not set, or when SMTP_PORT is not a valid integer.
+ /// Thrown when SMTP_HOST, SMTP_PORT, or SMTP_FROM_EMAIL is not set,
+ /// or when SMTP_PORT is not a valid integer.
///
public SmtpEmailProvider(IConfiguration configuration)
{
- _host =
- configuration["SMTP_HOST"]
- ?? throw new InvalidOperationException("SMTP_HOST environment variable is not set");
+ _host = ConfigurationHelpers.GetKeyOrThrow(configuration, ConfigurationKeys.SmtpHost);
- string portString = configuration["SMTP_PORT"] ?? "587";
- if (!int.TryParse(portString, out _port))
- throw new InvalidOperationException($"SMTP_PORT '{portString}' is not a valid integer");
+ _port = ConfigurationHelpers.GetIntKeyOrThrow(configuration, ConfigurationKeys.SmtpPort);
- _username = configuration["SMTP_USERNAME"];
- _password = configuration["SMTP_PASSWORD"];
+ _username = configuration[ConfigurationKeys.SmtpUsername];
+ _password = configuration[ConfigurationKeys.SmtpPassword];
- string useSslString = configuration["SMTP_USE_SSL"] ?? "true";
+ string useSslString = configuration[ConfigurationKeys.SmtpUseSsl] ?? "true";
_useSsl = bool.Parse(useSslString);
- _fromEmail =
- configuration["SMTP_FROM_EMAIL"]
- ?? throw new InvalidOperationException(
- "SMTP_FROM_EMAIL environment variable is not set"
- );
+ _fromEmail = ConfigurationHelpers.GetKeyOrThrow(
+ configuration,
+ ConfigurationKeys.SmtpFromEmail
+ );
- _fromName = configuration["SMTP_FROM_NAME"] ?? "The Biergarten";
+ _fromName = configuration[ConfigurationKeys.SmtpFromName] ?? "The Biergarten";
}
///