From 185b8156ec0afa06f138bd16b37db48e52cd0bd2 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 12 Sep 2025 17:05:01 +0200 Subject: [PATCH 1/8] Initial work --- .../IntegrationTestWebAppFactory.cs | 2 + API/Controller/Account/Login.cs | 4 +- API/Controller/Account/LoginV2.cs | 4 +- API/Controller/Account/Logout.cs | 8 +- API/Controller/Version/_ApiController.cs | 19 ++-- API/Program.cs | 21 ++-- API/Services/Account/AccountService.cs | 4 +- Common/Extensions/ConfigurationExtensions.cs | 106 +++++++++++++++--- Common/OpenShockMiddlewareHelper.cs | 2 +- Common/OpenShockServiceHelper.cs | 42 ------- Common/Options/DatabaseOptions.cs | 20 +--- Common/Options/FrontendOptions.cs | 24 +--- Common/Options/MetricsOptions.cs | 15 +-- Common/Options/RedisOptions.cs | 31 ----- Cron/Program.cs | 13 +-- LiveControlGateway/Program.cs | 13 +-- SeedE2E/Program.cs | 16 +-- 17 files changed, 144 insertions(+), 200 deletions(-) delete mode 100644 Common/Options/RedisOptions.cs diff --git a/API.IntegrationTests/IntegrationTestWebAppFactory.cs b/API.IntegrationTests/IntegrationTestWebAppFactory.cs index cffb6703..26ce85e5 100644 --- a/API.IntegrationTests/IntegrationTestWebAppFactory.cs +++ b/API.IntegrationTests/IntegrationTestWebAppFactory.cs @@ -46,6 +46,8 @@ public async Task InitializeAsync() { "OPENSHOCK__REDIS__PASSWORD", "" }, { "OPENSHOCK__REDIS__PORT", "6379" }, + { "OPENSHOCK__METRICS__ALLOWEDNETWORKS__0", "127.0.0.0/8" }, + { "OPENSHOCK__FRONTEND__BASEURL", "https://openshock.app" }, { "OPENSHOCK__FRONTEND__SHORTURL", "https://openshock.app" }, { "OPENSHOCK__FRONTEND__COOKIEDOMAIN", "openshock.app" }, diff --git a/API/Controller/Account/Login.cs b/API/Controller/Account/Login.cs index f8027a12..6a710613 100644 --- a/API/Controller/Account/Login.cs +++ b/API/Controller/Account/Login.cs @@ -28,10 +28,10 @@ public sealed partial class AccountController [MapToApiVersion("1")] public async Task Login( [FromBody] Login body, - [FromServices] IOptions options, + [FromServices] FrontendOptions options, CancellationToken cancellationToken) { - var cookieDomainToUse = options.Value.CookieDomain.Split(',').FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); + var cookieDomainToUse = options.CookieDomain.Split(',').FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); if (cookieDomainToUse is null) return Problem(LoginError.InvalidDomain); var loginAction = await _accountService.CreateUserLoginSessionAsync(body.Email, body.Password, new LoginContext diff --git a/API/Controller/Account/LoginV2.cs b/API/Controller/Account/LoginV2.cs index 6ab58532..971b2fb0 100644 --- a/API/Controller/Account/LoginV2.cs +++ b/API/Controller/Account/LoginV2.cs @@ -33,10 +33,10 @@ public sealed partial class AccountController public async Task LoginV2( [FromBody] LoginV2 body, [FromServices] ICloudflareTurnstileService turnstileService, - [FromServices] IOptions options, + [FromServices] FrontendOptions options, CancellationToken cancellationToken) { - var cookieDomainToUse = options.Value.CookieDomain.Split(',').FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); + var cookieDomainToUse = options.CookieDomain.Split(',').FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); if (cookieDomainToUse is null) return Problem(LoginError.InvalidDomain); var remoteIP = HttpContext.GetRemoteIP(); diff --git a/API/Controller/Account/Logout.cs b/API/Controller/Account/Logout.cs index d689299d..e7e27c59 100644 --- a/API/Controller/Account/Logout.cs +++ b/API/Controller/Account/Logout.cs @@ -14,10 +14,8 @@ public sealed partial class AccountController [MapToApiVersion("1")] public async Task Logout( [FromServices] ISessionService sessionService, - [FromServices] IOptions options) + [FromServices] FrontendOptions options) { - var config = options.Value; - // Remove session if valid if (HttpContext.TryGetUserSessionToken(out var sessionToken)) { @@ -25,14 +23,14 @@ public async Task Logout( } // Make sure cookie is removed, no matter if authenticated or not - var cookieDomainToUse = config.CookieDomain.Split(',').FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); + var cookieDomainToUse = options.CookieDomain.Split(',').FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); if (cookieDomainToUse is not null) { HttpContext.RemoveSessionKeyCookie("." + cookieDomainToUse); } else // Fallback to all domains { - foreach (var domain in config.CookieDomain.Split(',')) + foreach (var domain in options.CookieDomain.Split(',')) { HttpContext.RemoveSessionKeyCookie("." + domain); } diff --git a/API/Controller/Version/_ApiController.cs b/API/Controller/Version/_ApiController.cs index b87da6af..2f14361a 100644 --- a/API/Controller/Version/_ApiController.cs +++ b/API/Controller/Version/_ApiController.cs @@ -26,29 +26,29 @@ public sealed partial class VersionController : OpenShockControllerBase /// /// The version was successfully retrieved. [HttpGet] - public LegacyDataResponse GetBackendVersion( - [FromServices] IOptions frontendOptions, + public LegacyDataResponse GetBackendInfo( + [FromServices] FrontendOptions frontendOptions, [FromServices] IOptions turnstileOptions ) { - var frontendConfig = frontendOptions.Value; var turnstileConfig = turnstileOptions.Value; - return new( - new ApiVersionResponse + return new LegacyDataResponse( + new BackendInfoResponse { Version = OpenShockBackendVersion, Commit = GitHashAttribute.FullHash, CurrentTime = DateTimeOffset.UtcNow, - FrontendUrl = frontendConfig.BaseUrl, - ShortLinkUrl = frontendConfig.ShortUrl, - TurnstileSiteKey = turnstileConfig.SiteKey + FrontendUrl = frontendOptions.BaseUrl, + ShortLinkUrl = frontendOptions.ShortUrl, + TurnstileSiteKey = turnstileConfig.SiteKey, + IsUserAuthenticated = HttpContext.TryGetUserSessionToken(out _) }, "OpenShock" ); } - public sealed class ApiVersionResponse + public sealed class BackendInfoResponse { public required string Version { get; init; } public required string Commit { get; init; } @@ -56,5 +56,6 @@ public sealed class ApiVersionResponse public required Uri FrontendUrl { get; init; } public required Uri ShortLinkUrl { get; init; } public required string? TurnstileSiteKey { get; init; } + public required bool IsUserAuthenticated { get; init; } } } \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs index fd6c6c1a..f54762a5 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -22,21 +22,18 @@ #region Config -builder.RegisterCommonOpenShockOptions(); - -builder.Services.Configure(builder.Configuration.GetRequiredSection(FrontendOptions.SectionName)); -builder.Services.AddSingleton, FrontendOptionsValidator>(); - -var databaseConfig = builder.Configuration.GetDatabaseOptions(); -var redisConfig = builder.Configuration.GetRedisConfigurationOptions(); +var redisOptions = builder.RegisterRedisOptions(); +var databaseOptions = builder.RegisterDatabaseOptions(); +builder.RegisterMetricsOptions(); +builder.RegisterFrontendOptions(); #endregion builder.Services - .AddOpenShockMemDB(redisConfig) - .AddOpenShockDB(databaseConfig) + .AddOpenShockMemDB(redisOptions) + .AddOpenShockDB(databaseOptions) .AddOpenShockServices() - .AddOpenShockSignalR(redisConfig); + .AddOpenShockSignalR(redisOptions); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -59,9 +56,9 @@ await app.UseCommonOpenShockMiddleware(); -if (!databaseConfig.SkipMigration) +if (!databaseOptions.SkipMigration) { - await app.ApplyPendingOpenShockMigrations(databaseConfig); + await app.ApplyPendingOpenShockMigrations(databaseOptions); } else { diff --git a/API/Services/Account/AccountService.cs b/API/Services/Account/AccountService.cs index 9bd816f9..019099c0 100644 --- a/API/Services/Account/AccountService.cs +++ b/API/Services/Account/AccountService.cs @@ -35,12 +35,12 @@ public sealed class AccountService : IAccountService /// /// public AccountService(OpenShockContext db, IEmailService emailService, - ISessionService sessionService, ILogger logger, IOptions options) + ISessionService sessionService, ILogger logger, FrontendOptions options) { _db = db; _emailService = emailService; _logger = logger; - _frontendConfig = options.Value; + _frontendConfig = options; _sessionService = sessionService; } diff --git a/Common/Extensions/ConfigurationExtensions.cs b/Common/Extensions/ConfigurationExtensions.cs index 92fb5435..fbf1b63e 100644 --- a/Common/Extensions/ConfigurationExtensions.cs +++ b/Common/Extensions/ConfigurationExtensions.cs @@ -1,26 +1,100 @@ -using Microsoft.Extensions.Options; -using OpenShock.Common.Options; +using OpenShock.Common.Options; +using StackExchange.Redis; namespace OpenShock.Common.Extensions; public static class ConfigurationExtensions { - public static WebApplicationBuilder RegisterCommonOpenShockOptions(this WebApplicationBuilder builder) + // Reusable helper: bind or throw with a clear message + private static T GetRequired(this ConfigurationManager configuration, string path, string? example = null) where T : class { - if (builder.Environment.IsDevelopment()) + var section = configuration.GetSection(path); + var value = section.Get(); + if (value is null) { - Console.WriteLine(builder.Configuration.GetDebugView()); + var hint = string.IsNullOrWhiteSpace(example) ? "" : $" Example: {example}"; + throw new InvalidOperationException( + $"Missing or invalid configuration at '{path}'.{hint}" + ); } - - builder.Services.Configure(builder.Configuration.GetRequiredSection(DatabaseOptions.SectionName)); - builder.Services.AddSingleton, DatabaseOptionsValidator>(); - - builder.Services.Configure(builder.Configuration.GetRequiredSection(RedisOptions.SectionName)); - builder.Services.AddSingleton, RedisOptionsValidator>(); - - builder.Services.Configure(builder.Configuration.GetSection(MetricsOptions.SectionName)); - builder.Services.AddSingleton, MetricsOptionsValidator>(); - - return builder; + return value; + } + + public static DatabaseOptions RegisterDatabaseOptions(this WebApplicationBuilder builder) + { + // If DatabaseOptions has required fields, you can validate them after binding. + var options = builder.Configuration.GetRequired( + "OpenShock:DB", + """{ "ConnectionString": "..." }""" + ); + + builder.Services.AddSingleton(options); + return options; + } + + private sealed record RedisSection(string? Conn, string? User, string? Password, string? Host, string? Port); + public static ConfigurationOptions RegisterRedisOptions(this WebApplicationBuilder builder) + { + var section = builder.Configuration.GetRequired( + "OpenShock:Redis", + """{ "Conn": "redis://user:pass@host:6379" } or { "Host": "host", "Password": "...", "Port": "6379" }""" + ); + + ConfigurationOptions options; + + if (!string.IsNullOrWhiteSpace(section.Conn)) + { + options = ConfigurationOptions.Parse(section.Conn); + } + else + { + if (string.IsNullOrWhiteSpace(section.Host)) + throw new ArgumentException("Redis Host is required (OpenShock:Redis:Host)."); + + if (string.IsNullOrWhiteSpace(section.Password)) + throw new ArgumentException("Redis Password is required (OpenShock:Redis:Password)."); + + // Parse port with sane default + validation + ushort port = 6379; + if (!string.IsNullOrWhiteSpace(section.Port)) + { + if (!ushort.TryParse(section.Port, out port) || port == 0) + throw new ArgumentException("Redis Port must be a number between 1 and 65535 (OpenShock:Redis:Port)."); + } + + options = new ConfigurationOptions + { + User = section.User, // optional; only if ACLs enabled + Password = section.Password, + Ssl = false, // flip via connection string if needed + }; + options.EndPoints.Add(section.Host!, port); + } + + // Sensible defaults (adjust to taste) + options.AbortOnConnectFail = true; + + builder.Services.AddSingleton(options); + return options; + } + + public static MetricsOptions RegisterMetricsOptions(this WebApplicationBuilder builder) + { + var options = builder.Configuration.GetRequired( + "OpenShock:Metrics" + ); + + builder.Services.AddSingleton(options); + return options; + } + + public static FrontendOptions RegisterFrontendOptions(this WebApplicationBuilder builder) + { + var options = builder.Configuration.GetRequired( + "OpenShock:Frontend" + ); + + builder.Services.AddSingleton(options); + return options; } } diff --git a/Common/OpenShockMiddlewareHelper.cs b/Common/OpenShockMiddlewareHelper.cs index 43c905f7..712de703 100644 --- a/Common/OpenShockMiddlewareHelper.cs +++ b/Common/OpenShockMiddlewareHelper.cs @@ -25,7 +25,7 @@ public static class OpenShockMiddlewareHelper public static async Task UseCommonOpenShockMiddleware(this WebApplication app) { - var metricsOptions = app.Services.GetRequiredService>().Value; + var metricsOptions = app.Services.GetRequiredService(); var metricsAllowedIpNetworks = metricsOptions.AllowedNetworks.Select(x => IPNetwork.Parse(x)).ToArray(); foreach (var proxy in await TrustedProxiesFetcher.GetTrustedNetworksAsync()) diff --git a/Common/OpenShockServiceHelper.cs b/Common/OpenShockServiceHelper.cs index c845791e..d930379d 100644 --- a/Common/OpenShockServiceHelper.cs +++ b/Common/OpenShockServiceHelper.cs @@ -34,48 +34,6 @@ namespace OpenShock.Common; public static class OpenShockServiceHelper { - public static DatabaseOptions GetDatabaseOptions(this ConfigurationManager configuration) - { - var section = configuration.GetRequiredSection(DatabaseOptions.SectionName); - if (!section.Exists()) throw new Exception("TODO"); - - return section.Get() ?? throw new Exception("TODO"); - } - - public static ConfigurationOptions GetRedisConfigurationOptions(this ConfigurationManager configuration) - { - var section = configuration.GetRequiredSection(RedisOptions.SectionName); - if (!section.Exists()) throw new Exception("TODO"); - - var options = section.Get() ?? throw new Exception("TODO"); - - - ConfigurationOptions configurationOptions; - - if (string.IsNullOrWhiteSpace(options.Conn)) - { - configurationOptions = new ConfigurationOptions - { - AbortOnConnectFail = true, - Password = options.Password, - User = options.User, - Ssl = false, - EndPoints = new EndPointCollection - { - { options.Host, options.Port } - } - }; - } - else - { - configurationOptions = ConfigurationOptions.Parse(options.Conn); - } - - configurationOptions.AbortOnConnectFail = true; - - return configurationOptions; - } - public static IServiceCollection AddOpenShockMemDB(this IServiceCollection services, ConfigurationOptions options) { // <---- Redis ----> diff --git a/Common/Options/DatabaseOptions.cs b/Common/Options/DatabaseOptions.cs index 545b3779..cd8f77e6 100644 --- a/Common/Options/DatabaseOptions.cs +++ b/Common/Options/DatabaseOptions.cs @@ -1,19 +1,3 @@ -using Microsoft.Extensions.Options; -using System.ComponentModel.DataAnnotations; +namespace OpenShock.Common.Options; -namespace OpenShock.Common.Options; - -public sealed class DatabaseOptions -{ - public const string SectionName = "OpenShock:DB"; - - [Required(AllowEmptyStrings = false)] - public required string Conn { get; init; } - public bool SkipMigration { get; init; } = false; - public bool Debug { get; init; } = false; -} - -[OptionsValidator] -public partial class DatabaseOptionsValidator : IValidateOptions -{ -} \ No newline at end of file +public sealed record DatabaseOptions(string Conn, bool SkipMigration = false, bool Debug = false); \ No newline at end of file diff --git a/Common/Options/FrontendOptions.cs b/Common/Options/FrontendOptions.cs index bff96817..0f24cc1a 100644 --- a/Common/Options/FrontendOptions.cs +++ b/Common/Options/FrontendOptions.cs @@ -1,23 +1,3 @@ -using Microsoft.Extensions.Options; -using System.ComponentModel.DataAnnotations; +namespace OpenShock.Common.Options; -namespace OpenShock.Common.Options; - -public sealed class FrontendOptions -{ - public const string SectionName = "OpenShock:Frontend"; - - [Required] - public required Uri BaseUrl { get; init; } - - [Required] - public required Uri ShortUrl { get; init; } - - [Required(AllowEmptyStrings = false)] - public required string CookieDomain { get; init; } -} - -[OptionsValidator] -public partial class FrontendOptionsValidator : IValidateOptions -{ -} \ No newline at end of file +public sealed record FrontendOptions(Uri BaseUrl, Uri ShortUrl, string CookieDomain); \ No newline at end of file diff --git a/Common/Options/MetricsOptions.cs b/Common/Options/MetricsOptions.cs index d32314cf..3bf243b0 100644 --- a/Common/Options/MetricsOptions.cs +++ b/Common/Options/MetricsOptions.cs @@ -1,21 +1,8 @@ -using Microsoft.Extensions.Options; -using OpenShock.Common.Utils; +using OpenShock.Common.Utils; namespace OpenShock.Common.Options; public sealed class MetricsOptions { - public const string SectionName = "OpenShock:Metrics"; - public IReadOnlyCollection AllowedNetworks { get; init; } = TrustedProxiesFetcher.PrivateNetworks; -} - -public class MetricsOptionsValidator : IValidateOptions -{ - public ValidateOptionsResult Validate(string? name, MetricsOptions options) - { - var builder = new ValidateOptionsResultBuilder(); - - return builder.Build(); - } } \ No newline at end of file diff --git a/Common/Options/RedisOptions.cs b/Common/Options/RedisOptions.cs deleted file mode 100644 index 42badfb9..00000000 --- a/Common/Options/RedisOptions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.Extensions.Options; - -namespace OpenShock.Common.Options; - -public sealed class RedisOptions -{ - public const string SectionName = "OpenShock:Redis"; - - public required string Conn { get; set; } - public required string Host { get; init; } = string.Empty; - public string User { get; init; } = string.Empty; - public string Password { get; init; } = string.Empty; - public ushort Port { get; init; } = 6379; -} - -public sealed class RedisOptionsValidator : IValidateOptions -{ - public ValidateOptionsResult Validate(string? name, RedisOptions options) - { - ValidateOptionsResultBuilder builder = new ValidateOptionsResultBuilder(); - - if (string.IsNullOrEmpty(options.Conn)) - { - if (string.IsNullOrEmpty(options.Host)) builder.AddError("Host field is required if no connectionstring is specified", nameof(options.Host)); - if (string.IsNullOrEmpty(options.User)) builder.AddError("User field is required if no connectionstring is specified", nameof(options.Host)); - if (!string.IsNullOrEmpty(options.Password)) builder.AddError("Password field is required if no connectionstring is specified", nameof(options.Host)); - } - - return builder.Build(); - } -} \ No newline at end of file diff --git a/Cron/Program.cs b/Cron/Program.cs index d74f4f97..43b5cbb2 100644 --- a/Cron/Program.cs +++ b/Cron/Program.cs @@ -8,18 +8,17 @@ var builder = OpenShockApplication.CreateDefaultBuilder(args); -builder.RegisterCommonOpenShockOptions(); +var redisOptions = builder.RegisterRedisOptions(); +var databaseOptions = builder.RegisterDatabaseOptions(); +builder.RegisterMetricsOptions(); -var databaseConfig = builder.Configuration.GetDatabaseOptions(); -var redisConfig = builder.Configuration.GetRedisConfigurationOptions(); - -builder.Services.AddOpenShockMemDB(redisConfig); -builder.Services.AddOpenShockDB(databaseConfig); +builder.Services.AddOpenShockMemDB(redisOptions); +builder.Services.AddOpenShockDB(databaseOptions); builder.Services.AddOpenShockServices(); builder.Services.AddHangfire(hangfire => hangfire.UsePostgreSqlStorage(c => - c.UseNpgsqlConnection(databaseConfig.Conn))); + c.UseNpgsqlConnection(databaseOptions.Conn))); builder.Services.AddHangfireServer(); builder.AddSwaggerExt(); diff --git a/LiveControlGateway/Program.cs b/LiveControlGateway/Program.cs index 389e083b..5989d75e 100644 --- a/LiveControlGateway/Program.cs +++ b/LiveControlGateway/Program.cs @@ -12,19 +12,18 @@ var builder = OpenShockApplication.CreateDefaultBuilder(args); -builder.RegisterCommonOpenShockOptions(); +var redisOptions = builder.RegisterRedisOptions(); +var databaseOptions = builder.RegisterDatabaseOptions(); +builder.RegisterMetricsOptions(); builder.Services.Configure(builder.Configuration.GetRequiredSection(LcgOptions.SectionName)); builder.Services.AddSingleton, LcgOptionsValidator>(); -var databaseConfig = builder.Configuration.GetDatabaseOptions(); -var redisConfig = builder.Configuration.GetRedisConfigurationOptions(); - builder.Services - .AddOpenShockMemDB(redisConfig) - .AddOpenShockDB(databaseConfig) + .AddOpenShockMemDB(redisOptions) + .AddOpenShockDB(databaseOptions) .AddOpenShockServices() - .AddOpenShockSignalR(redisConfig); + .AddOpenShockSignalR(redisOptions); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/SeedE2E/Program.cs b/SeedE2E/Program.cs index e1fe30a6..bbe599e1 100644 --- a/SeedE2E/Program.cs +++ b/SeedE2E/Program.cs @@ -16,19 +16,15 @@ Console.WriteLine(builder.Configuration.GetDebugView()); } -builder.RegisterCommonOpenShockOptions(); +var databaseOptions = builder.RegisterDatabaseOptions(); +builder.RegisterFrontendOptions(); -builder.Services.Configure(builder.Configuration.GetRequiredSection(FrontendOptions.SectionName)); -builder.Services.AddSingleton, FrontendOptionsValidator>(); - -var databaseConfig = builder.Configuration.GetDatabaseOptions(); - -builder.Services.AddOpenShockDB(databaseConfig); +builder.Services.AddOpenShockDB(databaseOptions); builder.Services.AddOpenShockServices(); var app = builder.Build(); -await app.ApplyPendingOpenShockMigrations(databaseConfig); +await app.ApplyPendingOpenShockMigrations(databaseOptions); // --- SEED ALL THE DATABASE TABLES --- using (var scope = app.Services.CreateScope()) @@ -36,8 +32,8 @@ using var loggerFactory = scope.ServiceProvider.GetRequiredService(); var logger = loggerFactory.CreateLogger("OpenShock.SeedE2E"); - using var db = scope.ServiceProvider.GetRequiredService(); - using var transaction = await db.Database.BeginTransactionAsync(); + await using var db = scope.ServiceProvider.GetRequiredService(); + await using var transaction = await db.Database.BeginTransactionAsync(); // Core entities await UserSeeder.SeedAsync(db, logger); From 7039364be381eec73a724f522358a19b0c6e91a8 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 12 Sep 2025 17:25:52 +0200 Subject: [PATCH 2/8] Revert unrelated changes --- API.IntegrationTests/IntegrationTestWebAppFactory.cs | 2 -- API/Controller/Version/_ApiController.cs | 10 ++++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/API.IntegrationTests/IntegrationTestWebAppFactory.cs b/API.IntegrationTests/IntegrationTestWebAppFactory.cs index 26ce85e5..cffb6703 100644 --- a/API.IntegrationTests/IntegrationTestWebAppFactory.cs +++ b/API.IntegrationTests/IntegrationTestWebAppFactory.cs @@ -46,8 +46,6 @@ public async Task InitializeAsync() { "OPENSHOCK__REDIS__PASSWORD", "" }, { "OPENSHOCK__REDIS__PORT", "6379" }, - { "OPENSHOCK__METRICS__ALLOWEDNETWORKS__0", "127.0.0.0/8" }, - { "OPENSHOCK__FRONTEND__BASEURL", "https://openshock.app" }, { "OPENSHOCK__FRONTEND__SHORTURL", "https://openshock.app" }, { "OPENSHOCK__FRONTEND__COOKIEDOMAIN", "openshock.app" }, diff --git a/API/Controller/Version/_ApiController.cs b/API/Controller/Version/_ApiController.cs index 2f14361a..8d4e7e48 100644 --- a/API/Controller/Version/_ApiController.cs +++ b/API/Controller/Version/_ApiController.cs @@ -26,15 +26,15 @@ public sealed partial class VersionController : OpenShockControllerBase /// /// The version was successfully retrieved. [HttpGet] - public LegacyDataResponse GetBackendInfo( + public LegacyDataResponse GetBackendVersion( [FromServices] FrontendOptions frontendOptions, [FromServices] IOptions turnstileOptions ) { var turnstileConfig = turnstileOptions.Value; - return new LegacyDataResponse( - new BackendInfoResponse + return new( + new ApiVersionResponse { Version = OpenShockBackendVersion, Commit = GitHashAttribute.FullHash, @@ -42,13 +42,12 @@ [FromServices] IOptions turnstileOptions FrontendUrl = frontendOptions.BaseUrl, ShortLinkUrl = frontendOptions.ShortUrl, TurnstileSiteKey = turnstileConfig.SiteKey, - IsUserAuthenticated = HttpContext.TryGetUserSessionToken(out _) }, "OpenShock" ); } - public sealed class BackendInfoResponse + public sealed class ApiVersionResponse { public required string Version { get; init; } public required string Commit { get; init; } @@ -56,6 +55,5 @@ public sealed class BackendInfoResponse public required Uri FrontendUrl { get; init; } public required Uri ShortLinkUrl { get; init; } public required string? TurnstileSiteKey { get; init; } - public required bool IsUserAuthenticated { get; init; } } } \ No newline at end of file From 781568fdb54134c01c307d69758d210a6ba2098e Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 12 Sep 2025 17:57:49 +0200 Subject: [PATCH 3/8] Fix up more stuff --- API/Controller/Account/Login.cs | 2 +- API/Controller/Account/LoginV2.cs | 2 +- API/Controller/Account/Logout.cs | 4 +- API/Program.cs | 4 -- Common/Extensions/ConfigurationExtensions.cs | 62 ++++++++++---------- Common/Options/DatabaseOptions.cs | 7 ++- Common/Options/FrontendOptions.cs | 7 ++- Common/Options/MetricsOptions.cs | 6 +- 8 files changed, 49 insertions(+), 45 deletions(-) diff --git a/API/Controller/Account/Login.cs b/API/Controller/Account/Login.cs index 6a710613..a95bed51 100644 --- a/API/Controller/Account/Login.cs +++ b/API/Controller/Account/Login.cs @@ -31,7 +31,7 @@ public async Task Login( [FromServices] FrontendOptions options, CancellationToken cancellationToken) { - var cookieDomainToUse = options.CookieDomain.Split(',').FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); + var cookieDomainToUse = options.CookieDomains.FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); if (cookieDomainToUse is null) return Problem(LoginError.InvalidDomain); var loginAction = await _accountService.CreateUserLoginSessionAsync(body.Email, body.Password, new LoginContext diff --git a/API/Controller/Account/LoginV2.cs b/API/Controller/Account/LoginV2.cs index 971b2fb0..6896227d 100644 --- a/API/Controller/Account/LoginV2.cs +++ b/API/Controller/Account/LoginV2.cs @@ -36,7 +36,7 @@ public async Task LoginV2( [FromServices] FrontendOptions options, CancellationToken cancellationToken) { - var cookieDomainToUse = options.CookieDomain.Split(',').FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); + var cookieDomainToUse = options.CookieDomains.FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); if (cookieDomainToUse is null) return Problem(LoginError.InvalidDomain); var remoteIP = HttpContext.GetRemoteIP(); diff --git a/API/Controller/Account/Logout.cs b/API/Controller/Account/Logout.cs index e7e27c59..35eb3f43 100644 --- a/API/Controller/Account/Logout.cs +++ b/API/Controller/Account/Logout.cs @@ -23,14 +23,14 @@ public async Task Logout( } // Make sure cookie is removed, no matter if authenticated or not - var cookieDomainToUse = options.CookieDomain.Split(',').FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); + var cookieDomainToUse = options.CookieDomains.FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase)); if (cookieDomainToUse is not null) { HttpContext.RemoveSessionKeyCookie("." + cookieDomainToUse); } else // Fallback to all domains { - foreach (var domain in options.CookieDomain.Split(',')) + foreach (var domain in options.CookieDomains) { HttpContext.RemoveSessionKeyCookie("." + domain); } diff --git a/API/Program.cs b/API/Program.cs index 09b7a109..61122bb9 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -21,15 +21,11 @@ var builder = OpenShockApplication.CreateDefaultBuilder(args); -#region Config - var redisOptions = builder.RegisterRedisOptions(); var databaseOptions = builder.RegisterDatabaseOptions(); builder.RegisterMetricsOptions(); builder.RegisterFrontendOptions(); -#endregion - builder.Services .AddOpenShockMemDB(redisOptions) .AddOpenShockDB(databaseOptions) diff --git a/Common/Extensions/ConfigurationExtensions.cs b/Common/Extensions/ConfigurationExtensions.cs index fbf1b63e..e8a75f77 100644 --- a/Common/Extensions/ConfigurationExtensions.cs +++ b/Common/Extensions/ConfigurationExtensions.cs @@ -1,44 +1,33 @@ using OpenShock.Common.Options; +using OpenShock.Common.Utils; using StackExchange.Redis; namespace OpenShock.Common.Extensions; public static class ConfigurationExtensions { - // Reusable helper: bind or throw with a clear message - private static T GetRequired(this ConfigurationManager configuration, string path, string? example = null) where T : class - { - var section = configuration.GetSection(path); - var value = section.Get(); - if (value is null) - { - var hint = string.IsNullOrWhiteSpace(example) ? "" : $" Example: {example}"; - throw new InvalidOperationException( - $"Missing or invalid configuration at '{path}'.{hint}" - ); - } - return value; - } - public static DatabaseOptions RegisterDatabaseOptions(this WebApplicationBuilder builder) { - // If DatabaseOptions has required fields, you can validate them after binding. - var options = builder.Configuration.GetRequired( - "OpenShock:DB", - """{ "ConnectionString": "..." }""" - ); + var options = builder.Configuration.GetRequiredSection("OpenShock:DB").Get(); + if (options is null) throw new Exception(); builder.Services.AddSingleton(options); return options; } - private sealed record RedisSection(string? Conn, string? User, string? Password, string? Host, string? Port); + private sealed class RedisSection + { + public string? Conn { get; init; } + public string? User { get; init; } + public string? Password { get; init; } + public string? Host { get; init; } + public string? Port { get; init; } + } + public static ConfigurationOptions RegisterRedisOptions(this WebApplicationBuilder builder) { - var section = builder.Configuration.GetRequired( - "OpenShock:Redis", - """{ "Conn": "redis://user:pass@host:6379" } or { "Host": "host", "Password": "...", "Port": "6379" }""" - ); + var section = builder.Configuration.GetRequiredSection("OpenShock:Redis").Get(); + if (section is null) throw new Exception(); ConfigurationOptions options; @@ -80,9 +69,10 @@ public static ConfigurationOptions RegisterRedisOptions(this WebApplicationBuild public static MetricsOptions RegisterMetricsOptions(this WebApplicationBuilder builder) { - var options = builder.Configuration.GetRequired( - "OpenShock:Metrics" - ); + var options = builder.Configuration.GetSection("OpenShock:Metrics").Get() ?? new MetricsOptions + { + AllowedNetworks = TrustedProxiesFetcher.PrivateNetworks + }; builder.Services.AddSingleton(options); return options; @@ -90,11 +80,21 @@ public static MetricsOptions RegisterMetricsOptions(this WebApplicationBuilder b public static FrontendOptions RegisterFrontendOptions(this WebApplicationBuilder builder) { - var options = builder.Configuration.GetRequired( - "OpenShock:Frontend" - ); + var section = builder.Configuration.GetRequiredSection("OpenShock:Frontend"); + + var options = new FrontendOptions + { + BaseUrl = section.GetValue("BaseUrl") ?? throw new Exception("Frontend Url is required."), + ShortUrl = section.GetValue("ShortUrl") ?? throw new Exception("Frontend Url is required."), + CookieDomains = SplitCsv(section["CookieDomain"] ?? throw new Exception("Frontend Domain is required.")), + }; builder.Services.AddSingleton(options); return options; + + static string[] SplitCsv(string csv) + { + return csv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } } } diff --git a/Common/Options/DatabaseOptions.cs b/Common/Options/DatabaseOptions.cs index cd8f77e6..b98e3154 100644 --- a/Common/Options/DatabaseOptions.cs +++ b/Common/Options/DatabaseOptions.cs @@ -1,3 +1,8 @@ namespace OpenShock.Common.Options; -public sealed record DatabaseOptions(string Conn, bool SkipMigration = false, bool Debug = false); \ No newline at end of file +public sealed class DatabaseOptions +{ + public required string Conn { get; init; } + public required bool SkipMigration { get; init; } + public required bool Debug { get; init; } +} \ No newline at end of file diff --git a/Common/Options/FrontendOptions.cs b/Common/Options/FrontendOptions.cs index 0f24cc1a..6a41bac4 100644 --- a/Common/Options/FrontendOptions.cs +++ b/Common/Options/FrontendOptions.cs @@ -1,3 +1,8 @@ namespace OpenShock.Common.Options; -public sealed record FrontendOptions(Uri BaseUrl, Uri ShortUrl, string CookieDomain); \ No newline at end of file +public sealed class FrontendOptions +{ + public required Uri BaseUrl { get; init; } + public required Uri ShortUrl { get; init; } + public required IReadOnlyCollection CookieDomains { get; init; } +} \ No newline at end of file diff --git a/Common/Options/MetricsOptions.cs b/Common/Options/MetricsOptions.cs index 3bf243b0..6d4d672c 100644 --- a/Common/Options/MetricsOptions.cs +++ b/Common/Options/MetricsOptions.cs @@ -1,8 +1,6 @@ -using OpenShock.Common.Utils; - -namespace OpenShock.Common.Options; +namespace OpenShock.Common.Options; public sealed class MetricsOptions { - public IReadOnlyCollection AllowedNetworks { get; init; } = TrustedProxiesFetcher.PrivateNetworks; + public required IReadOnlyCollection AllowedNetworks { get; init; } } \ No newline at end of file From 164ac06e933e3caf1244c9aec2c213c8de538871 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 12 Sep 2025 18:23:03 +0200 Subject: [PATCH 4/8] revert some stuff --- API/Program.cs | 3 --- Common/Options/DatabaseOptions.cs | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/API/Program.cs b/API/Program.cs index 61122bb9..129fde32 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,7 +1,5 @@ using Microsoft.AspNetCore.Http.Connections; -using Microsoft.Extensions.Options; using OpenShock.API.Realtime; -using OpenShock.API.Services; using OpenShock.API.Services.Account; using OpenShock.API.Services.DeviceUpdate; using OpenShock.API.Services.Email; @@ -11,7 +9,6 @@ using OpenShock.Common.DeviceControl; using OpenShock.Common.Extensions; using OpenShock.Common.Hubs; -using OpenShock.Common.Options; using OpenShock.Common.Services; using OpenShock.Common.Services.Device; using OpenShock.Common.Services.LCGNodeProvisioner; diff --git a/Common/Options/DatabaseOptions.cs b/Common/Options/DatabaseOptions.cs index b98e3154..9bd12fd2 100644 --- a/Common/Options/DatabaseOptions.cs +++ b/Common/Options/DatabaseOptions.cs @@ -3,6 +3,6 @@ public sealed class DatabaseOptions { public required string Conn { get; init; } - public required bool SkipMigration { get; init; } - public required bool Debug { get; init; } + public bool SkipMigration { get; init; } = false; + public bool Debug { get; init; } = false; } \ No newline at end of file From 8f98292bfb2e1a6d2502c6d7eb21b2c6c6b22f6a Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 12 Sep 2025 18:25:58 +0200 Subject: [PATCH 5/8] Improve exceptions --- Common/Extensions/ConfigurationExtensions.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Common/Extensions/ConfigurationExtensions.cs b/Common/Extensions/ConfigurationExtensions.cs index e8a75f77..ae21adcf 100644 --- a/Common/Extensions/ConfigurationExtensions.cs +++ b/Common/Extensions/ConfigurationExtensions.cs @@ -9,7 +9,8 @@ public static class ConfigurationExtensions public static DatabaseOptions RegisterDatabaseOptions(this WebApplicationBuilder builder) { var options = builder.Configuration.GetRequiredSection("OpenShock:DB").Get(); - if (options is null) throw new Exception(); + if (options is null) + throw new InvalidOperationException("Missing or invalid configuration for OpenShock:DB."); builder.Services.AddSingleton(options); return options; @@ -27,7 +28,8 @@ private sealed class RedisSection public static ConfigurationOptions RegisterRedisOptions(this WebApplicationBuilder builder) { var section = builder.Configuration.GetRequiredSection("OpenShock:Redis").Get(); - if (section is null) throw new Exception(); + if (section is null) + throw new InvalidOperationException("Missing or invalid configuration for OpenShock:Redis."); ConfigurationOptions options; @@ -48,14 +50,14 @@ public static ConfigurationOptions RegisterRedisOptions(this WebApplicationBuild if (!string.IsNullOrWhiteSpace(section.Port)) { if (!ushort.TryParse(section.Port, out port) || port == 0) - throw new ArgumentException("Redis Port must be a number between 1 and 65535 (OpenShock:Redis:Port)."); + throw new InvalidOperationException("Redis Port must be a number between 1 and 65535 (OpenShock:Redis:Port)."); } options = new ConfigurationOptions { User = section.User, // optional; only if ACLs enabled Password = section.Password, - Ssl = false, // flip via connection string if needed + Ssl = false, // flip via connection string if needed }; options.EndPoints.Add(section.Host!, port); } @@ -84,9 +86,9 @@ public static FrontendOptions RegisterFrontendOptions(this WebApplicationBuilder var options = new FrontendOptions { - BaseUrl = section.GetValue("BaseUrl") ?? throw new Exception("Frontend Url is required."), - ShortUrl = section.GetValue("ShortUrl") ?? throw new Exception("Frontend Url is required."), - CookieDomains = SplitCsv(section["CookieDomain"] ?? throw new Exception("Frontend Domain is required.")), + BaseUrl = section.GetValue("BaseUrl") ?? throw new InvalidOperationException("Frontend BaseUrl is required (OpenShock:Frontend:BaseUrl)."), + ShortUrl = section.GetValue("ShortUrl") ?? throw new InvalidOperationException("Frontend ShortUrl is required (OpenShock:Frontend:ShortUrl)."), + CookieDomains = SplitCsv(section["CookieDomain"] ?? throw new InvalidOperationException("Frontend CookieDomain is required (OpenShock:Frontend:CookieDomain).")), }; builder.Services.AddSingleton(options); From 5f5cc26f7f81ecd6f994e6f72f1b26e44d1b5a42 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 12 Sep 2025 18:52:57 +0200 Subject: [PATCH 6/8] Do some validation --- Common/Extensions/ConfigurationExtensions.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Common/Extensions/ConfigurationExtensions.cs b/Common/Extensions/ConfigurationExtensions.cs index ae21adcf..051c0f6e 100644 --- a/Common/Extensions/ConfigurationExtensions.cs +++ b/Common/Extensions/ConfigurationExtensions.cs @@ -11,6 +11,8 @@ public static DatabaseOptions RegisterDatabaseOptions(this WebApplicationBuilder var options = builder.Configuration.GetRequiredSection("OpenShock:DB").Get(); if (options is null) throw new InvalidOperationException("Missing or invalid configuration for OpenShock:DB."); + + if (string.IsNullOrEmpty(options.Conn)) throw new InvalidOperationException("Missing or invalid connection string (OpenShock:DB:Conn)."); builder.Services.AddSingleton(options); return options; @@ -55,9 +57,9 @@ public static ConfigurationOptions RegisterRedisOptions(this WebApplicationBuild options = new ConfigurationOptions { - User = section.User, // optional; only if ACLs enabled + User = section.User, Password = section.Password, - Ssl = false, // flip via connection string if needed + Ssl = false, }; options.EndPoints.Add(section.Host!, port); } @@ -90,6 +92,8 @@ public static FrontendOptions RegisterFrontendOptions(this WebApplicationBuilder ShortUrl = section.GetValue("ShortUrl") ?? throw new InvalidOperationException("Frontend ShortUrl is required (OpenShock:Frontend:ShortUrl)."), CookieDomains = SplitCsv(section["CookieDomain"] ?? throw new InvalidOperationException("Frontend CookieDomain is required (OpenShock:Frontend:CookieDomain).")), }; + + if (options.CookieDomains.Count == 0) throw new InvalidOperationException("At least one cookie domain must be configured (OpenShock:Frontend:CookieDomain)."); builder.Services.AddSingleton(options); return options; From 54c342f5340fb913b370f92b1290a11a2f8b8dd1 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 12 Sep 2025 18:58:52 +0200 Subject: [PATCH 7/8] Fixed it --- Common/OpenShockServiceHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common/OpenShockServiceHelper.cs b/Common/OpenShockServiceHelper.cs index d930379d..9610a851 100644 --- a/Common/OpenShockServiceHelper.cs +++ b/Common/OpenShockServiceHelper.cs @@ -38,7 +38,7 @@ public static IServiceCollection AddOpenShockMemDB(this IServiceCollection servi { // <---- Redis ----> services.AddSingleton(ConnectionMultiplexer.Connect(options)); - services.AddSingleton(); + services.AddSingleton(serviceProvider => new RedisConnectionProvider(serviceProvider.GetRequiredService())); services.AddSingleton(); return services; From 95b5d026071eea7adbdd8b6de0b49ce2f540df2a Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 12 Sep 2025 19:21:56 +0200 Subject: [PATCH 8/8] Remove all IOptions --- API/Controller/Version/_ApiController.cs | 6 ++---- API/Services/Email/EmailServiceExtension.cs | 2 +- API/Services/Email/Mailjet/MailjetEmailService.cs | 10 +++++----- .../Email/Mailjet/MailjetEmailServiceExtension.cs | 3 +++ API/Services/Email/Smtp/SmtpEmailService.cs | 8 ++++---- API/Services/Email/Smtp/SmtpEmailServiceExtension.cs | 2 ++ API/Services/Turnstile/CloudflareTurnstileService.cs | 4 ++-- .../Turnstile/CloudflareTurnstileServiceExtensions.cs | 2 ++ LiveControlGateway/Controllers/HubControllerBase.cs | 4 ++-- LiveControlGateway/Controllers/HubV1Controller.cs | 2 +- LiveControlGateway/Controllers/HubV2Controller.cs | 2 +- .../Controllers/InstanceDetailsController.cs | 6 +++--- LiveControlGateway/LcgKeepAlive.cs | 4 ++-- LiveControlGateway/Program.cs | 2 ++ 14 files changed, 32 insertions(+), 25 deletions(-) diff --git a/API/Controller/Version/_ApiController.cs b/API/Controller/Version/_ApiController.cs index 38702d78..1eced173 100644 --- a/API/Controller/Version/_ApiController.cs +++ b/API/Controller/Version/_ApiController.cs @@ -39,11 +39,9 @@ private static string GetBackendVersion() [HttpGet] public LegacyDataResponse GetBackendInfo( [FromServices] FrontendOptions frontendOptions, - [FromServices] IOptions turnstileOptions + [FromServices] TurnstileOptions turnstileOptions ) { - var turnstileConfig = turnstileOptions.Value; - return new( new BackendInfoResponse { @@ -52,7 +50,7 @@ [FromServices] IOptions turnstileOptions CurrentTime = DateTimeOffset.UtcNow, FrontendUrl = frontendOptions.BaseUrl, ShortLinkUrl = frontendOptions.ShortUrl, - TurnstileSiteKey = turnstileConfig.SiteKey, + TurnstileSiteKey = turnstileOptions.SiteKey, IsUserAuthenticated = HttpContext.TryGetUserSessionToken(out _) }, "OpenShock" diff --git a/API/Services/Email/EmailServiceExtension.cs b/API/Services/Email/EmailServiceExtension.cs index 6aba76c0..f45913bd 100644 --- a/API/Services/Email/EmailServiceExtension.cs +++ b/API/Services/Email/EmailServiceExtension.cs @@ -36,7 +36,7 @@ public static WebApplicationBuilder AddEmailService(this WebApplicationBuilder b private static WebApplicationBuilder AddSenderContactConfiguration(this WebApplicationBuilder builder) { - builder.Services.Configure(builder.Configuration.GetRequiredSection(MailOptions.SenderSectionName)); + builder.Services.AddSingleton(builder.Configuration.GetRequiredSection(MailOptions.SenderSectionName).Get() ?? throw new NullReferenceException()); return builder; } } diff --git a/API/Services/Email/Mailjet/MailjetEmailService.cs b/API/Services/Email/Mailjet/MailjetEmailService.cs index 09551327..776da1b1 100644 --- a/API/Services/Email/Mailjet/MailjetEmailService.cs +++ b/API/Services/Email/Mailjet/MailjetEmailService.cs @@ -11,8 +11,8 @@ public sealed class MailjetEmailService : IEmailService, IDisposable { private readonly HttpClient _httpClient; private readonly MailJetOptions _options; - private readonly ILogger _logger; private readonly MailOptions.MailSenderContact _sender; + private readonly ILogger _logger; /// /// DI Constructor @@ -23,14 +23,14 @@ public sealed class MailjetEmailService : IEmailService, IDisposable /// public MailjetEmailService( HttpClient httpClient, - IOptions options, - IOptions sender, + MailJetOptions options, + MailOptions.MailSenderContact sender, ILogger logger ) { _httpClient = httpClient; - _sender = sender.Value; - _options = options.Value; + _options = options; + _sender = sender; _logger = logger; } diff --git a/API/Services/Email/Mailjet/MailjetEmailServiceExtension.cs b/API/Services/Email/Mailjet/MailjetEmailServiceExtension.cs index a6d1f4e2..31fe0758 100644 --- a/API/Services/Email/Mailjet/MailjetEmailServiceExtension.cs +++ b/API/Services/Email/Mailjet/MailjetEmailServiceExtension.cs @@ -11,8 +11,11 @@ public static WebApplicationBuilder AddMailjetEmailService(this WebApplicationBu { var section = builder.Configuration.GetRequiredSection(MailJetOptions.SectionName); + + // TODO Simplify this builder.Services.Configure(section); builder.Services.AddSingleton, MailJetOptionsValidator>(); + builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); var options = section.Get() ?? throw new NullReferenceException("MailJetOptions is null!"); var basicAuthValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{options.Key}:{options.Secret}")); diff --git a/API/Services/Email/Smtp/SmtpEmailService.cs b/API/Services/Email/Smtp/SmtpEmailService.cs index 3c0a290b..6ae09bc1 100644 --- a/API/Services/Email/Smtp/SmtpEmailService.cs +++ b/API/Services/Email/Smtp/SmtpEmailService.cs @@ -13,8 +13,8 @@ namespace OpenShock.API.Services.Email.Smtp; public sealed class SmtpEmailService : IEmailService { private readonly SmtpServiceTemplates _templates; - private readonly MailboxAddress _sender; private readonly SmtpOptions _options; + private readonly MailboxAddress _sender; private readonly ILogger _logger; private readonly TemplateOptions _templateOptions; @@ -24,19 +24,19 @@ public sealed class SmtpEmailService : IEmailService /// DI Constructor /// /// - /// /// + /// /// public SmtpEmailService( SmtpServiceTemplates templates, - IOptions sender, IOptions options, + MailOptions.MailSenderContact sender, ILogger logger ) { _templates = templates; - _sender = sender.Value.ToMailAddress(); _options = options.Value; + _sender = sender.ToMailAddress(); _logger = logger; // This class is will be registered as a singleton, static members are not needed diff --git a/API/Services/Email/Smtp/SmtpEmailServiceExtension.cs b/API/Services/Email/Smtp/SmtpEmailServiceExtension.cs index db89d09b..d20bb75d 100644 --- a/API/Services/Email/Smtp/SmtpEmailServiceExtension.cs +++ b/API/Services/Email/Smtp/SmtpEmailServiceExtension.cs @@ -9,8 +9,10 @@ public static WebApplicationBuilder AddSmtpEmailService(this WebApplicationBuild { var section = builder.Configuration.GetRequiredSection(SmtpOptions.SectionName); + // TODO Simplify this builder.Services.Configure(section); builder.Services.AddSingleton, SmtpOptionsValidator>(); + builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); builder.Services.AddSingleton(new SmtpServiceTemplates { diff --git a/API/Services/Turnstile/CloudflareTurnstileService.cs b/API/Services/Turnstile/CloudflareTurnstileService.cs index a90de2e9..e571cf30 100644 --- a/API/Services/Turnstile/CloudflareTurnstileService.cs +++ b/API/Services/Turnstile/CloudflareTurnstileService.cs @@ -16,10 +16,10 @@ public sealed class CloudflareTurnstileService : ICloudflareTurnstileService private readonly IHostEnvironment _environment; private readonly ILogger _logger; - public CloudflareTurnstileService(HttpClient httpClient, IOptions options, IHostEnvironment environment, ILogger logger) + public CloudflareTurnstileService(HttpClient httpClient, TurnstileOptions options, IHostEnvironment environment, ILogger logger) { _httpClient = httpClient; - _options = options.Value; + _options = options; _environment = environment; _logger = logger; } diff --git a/API/Services/Turnstile/CloudflareTurnstileServiceExtensions.cs b/API/Services/Turnstile/CloudflareTurnstileServiceExtensions.cs index c8d9daa5..ad18479f 100644 --- a/API/Services/Turnstile/CloudflareTurnstileServiceExtensions.cs +++ b/API/Services/Turnstile/CloudflareTurnstileServiceExtensions.cs @@ -10,8 +10,10 @@ public static WebApplicationBuilder AddCloudflareTurnstileService(this WebApplic { var section = builder.Configuration.GetRequiredSection(TurnstileOptions.Turnstile); + // TODO Simplify this builder.Services.Configure(section); builder.Services.AddSingleton, TurnstileOptionsValidator>(); + builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); builder.Services.AddHttpClient(client => { diff --git a/LiveControlGateway/Controllers/HubControllerBase.cs b/LiveControlGateway/Controllers/HubControllerBase.cs index 5b7b4c4c..192e8304 100644 --- a/LiveControlGateway/Controllers/HubControllerBase.cs +++ b/LiveControlGateway/Controllers/HubControllerBase.cs @@ -90,13 +90,13 @@ protected HubControllerBase( ISerializer outgoingSerializer, HubLifetimeManager hubLifetimeManager, IServiceProvider serviceProvider, - IOptions options, + LcgOptions options, ILogger> logger ) : base(logger, incomingSerializer, outgoingSerializer) { _hubLifetimeManager = hubLifetimeManager; ServiceProvider = serviceProvider; - _options = options.Value; + _options = options; _keepAliveTimeoutTimer.Elapsed += async (_, _) => { try diff --git a/LiveControlGateway/Controllers/HubV1Controller.cs b/LiveControlGateway/Controllers/HubV1Controller.cs index 505c1a01..55ab9e14 100644 --- a/LiveControlGateway/Controllers/HubV1Controller.cs +++ b/LiveControlGateway/Controllers/HubV1Controller.cs @@ -38,7 +38,7 @@ public HubV1Controller( HubLifetimeManager hubLifetimeManager, IHubContext userHubContext, IServiceProvider serviceProvider, - IOptions options, + LcgOptions options, ILogger logger ) : base(HubToGatewayMessage.Serializer, GatewayToHubMessage.Serializer, hubLifetimeManager, serviceProvider, options, logger) diff --git a/LiveControlGateway/Controllers/HubV2Controller.cs b/LiveControlGateway/Controllers/HubV2Controller.cs index e5535287..637fca9a 100644 --- a/LiveControlGateway/Controllers/HubV2Controller.cs +++ b/LiveControlGateway/Controllers/HubV2Controller.cs @@ -43,7 +43,7 @@ public HubV2Controller( HubLifetimeManager hubLifetimeManager, IHubContext userHubContext, IServiceProvider serviceProvider, - IOptions options, + LcgOptions options, ILogger logger ) : base(HubToGatewayMessage.Serializer, GatewayToHubMessage.Serializer, hubLifetimeManager, serviceProvider, options, logger) diff --git a/LiveControlGateway/Controllers/InstanceDetailsController.cs b/LiveControlGateway/Controllers/InstanceDetailsController.cs index 0f779d5b..d055a63f 100644 --- a/LiveControlGateway/Controllers/InstanceDetailsController.cs +++ b/LiveControlGateway/Controllers/InstanceDetailsController.cs @@ -28,7 +28,7 @@ public sealed class InstanceDetailsController : OpenShockControllerBase [HttpGet] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [MapToApiVersion("1")] - public InstanceDetailsResponse GetNodeInfo([FromServices] IOptions options) + public InstanceDetailsResponse GetNodeInfo([FromServices] LcgOptions options) { return new InstanceDetailsResponse { @@ -36,8 +36,8 @@ public InstanceDetailsResponse GetNodeInfo([FromServices] IOptions o Version = AssemblyVersion, Commit = GitHashAttribute.FullHash, CurrentTime = DateTimeOffset.UtcNow, - Fqdn = options.Value.Fqdn, - CountryCode = options.Value.CountryCode + Fqdn = options.Fqdn, + CountryCode = options.CountryCode }; } diff --git a/LiveControlGateway/LcgKeepAlive.cs b/LiveControlGateway/LcgKeepAlive.cs index 25b7f126..63ae388b 100644 --- a/LiveControlGateway/LcgKeepAlive.cs +++ b/LiveControlGateway/LcgKeepAlive.cs @@ -26,11 +26,11 @@ public sealed class LcgKeepAlive : IHostedService /// /// /// - public LcgKeepAlive(IRedisConnectionProvider redisConnectionProvider, IWebHostEnvironment env, IOptions options, ILogger logger) + public LcgKeepAlive(IRedisConnectionProvider redisConnectionProvider, IWebHostEnvironment env, LcgOptions options, ILogger logger) { _redisConnectionProvider = redisConnectionProvider; _env = env; - _options = options.Value; + _options = options; _logger = logger; } diff --git a/LiveControlGateway/Program.cs b/LiveControlGateway/Program.cs index 5989d75e..f01d350f 100644 --- a/LiveControlGateway/Program.cs +++ b/LiveControlGateway/Program.cs @@ -16,8 +16,10 @@ var databaseOptions = builder.RegisterDatabaseOptions(); builder.RegisterMetricsOptions(); +// TODO Simplify this builder.Services.Configure(builder.Configuration.GetRequiredSection(LcgOptions.SectionName)); builder.Services.AddSingleton, LcgOptionsValidator>(); +builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); builder.Services .AddOpenShockMemDB(redisOptions)