Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions API/Controller/Account/Login.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ public sealed partial class AccountController
[MapToApiVersion("1")]
public async Task<IActionResult> Login(
[FromBody] Login body,
[FromServices] IOptions<FrontendOptions> 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.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
Expand Down
4 changes: 2 additions & 2 deletions API/Controller/Account/LoginV2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ public sealed partial class AccountController
public async Task<IActionResult> LoginV2(
[FromBody] LoginV2 body,
[FromServices] ICloudflareTurnstileService turnstileService,
[FromServices] IOptions<FrontendOptions> 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.CookieDomains.FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase));
if (cookieDomainToUse is null) return Problem(LoginError.InvalidDomain);

var remoteIP = HttpContext.GetRemoteIP();
Expand Down
8 changes: 3 additions & 5 deletions API/Controller/Account/Logout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,23 @@ public sealed partial class AccountController
[MapToApiVersion("1")]
public async Task<IActionResult> Logout(
[FromServices] ISessionService sessionService,
[FromServices] IOptions<FrontendOptions> options)
[FromServices] FrontendOptions options)
{
var config = options.Value;

// Remove session if valid
if (HttpContext.TryGetUserSessionToken(out var sessionToken))
{
await sessionService.DeleteSessionByTokenAsync(sessionToken);
}

// 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.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 config.CookieDomain.Split(','))
foreach (var domain in options.CookieDomains)
{
HttpContext.RemoveSessionKeyCookie("." + domain);
}
Expand Down
13 changes: 5 additions & 8 deletions API/Controller/Version/_ApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,19 @@ private static string GetBackendVersion()
/// <response code="200">The version was successfully retrieved.</response>
[HttpGet]
public LegacyDataResponse<BackendInfoResponse> GetBackendInfo(
[FromServices] IOptions<FrontendOptions> frontendOptions,
[FromServices] IOptions<TurnstileOptions> turnstileOptions
[FromServices] FrontendOptions frontendOptions,
[FromServices] TurnstileOptions turnstileOptions
)
{
var frontendConfig = frontendOptions.Value;
var turnstileConfig = turnstileOptions.Value;

return new(
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 = turnstileOptions.SiteKey,
IsUserAuthenticated = HttpContext.TryGetUserSessionToken(out _)
},
"OpenShock"
Expand Down
28 changes: 9 additions & 19 deletions API/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -21,23 +18,16 @@

var builder = OpenShockApplication.CreateDefaultBuilder<Program>(args);

#region Config

builder.RegisterCommonOpenShockOptions();

builder.Services.Configure<FrontendOptions>(builder.Configuration.GetRequiredSection(FrontendOptions.SectionName));
builder.Services.AddSingleton<IValidateOptions<FrontendOptions>, FrontendOptionsValidator>();

var databaseConfig = builder.Configuration.GetDatabaseOptions();
var redisConfig = builder.Configuration.GetRedisConfigurationOptions();

#endregion
var redisOptions = builder.RegisterRedisOptions();
var databaseOptions = builder.RegisterDatabaseOptions();
builder.RegisterMetricsOptions();
builder.RegisterFrontendOptions();

builder.Services
.AddOpenShockMemDB(redisConfig)
.AddOpenShockDB(databaseConfig)
.AddOpenShockMemDB(redisOptions)
.AddOpenShockDB(databaseOptions)
.AddOpenShockServices()
.AddOpenShockSignalR(redisConfig);
.AddOpenShockSignalR(redisOptions);

builder.Services.AddScoped<IDeviceService, DeviceService>();
builder.Services.AddScoped<IControlSender, ControlSender>();
Expand All @@ -60,9 +50,9 @@

await app.UseCommonOpenShockMiddleware();

if (!databaseConfig.SkipMigration)
if (!databaseOptions.SkipMigration)
{
await app.ApplyPendingOpenShockMigrations(databaseConfig);
await app.ApplyPendingOpenShockMigrations(databaseOptions);
}
else
{
Expand Down
4 changes: 2 additions & 2 deletions API/Services/Account/AccountService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ public sealed class AccountService : IAccountService
/// <param name="logger"></param>
/// <param name="options"></param>
public AccountService(OpenShockContext db, IEmailService emailService,
ISessionService sessionService, ILogger<AccountService> logger, IOptions<FrontendOptions> options)
ISessionService sessionService, ILogger<AccountService> logger, FrontendOptions options)
{
_db = db;
_emailService = emailService;
_logger = logger;
_frontendConfig = options.Value;
_frontendConfig = options;
_sessionService = sessionService;
}

Expand Down
2 changes: 1 addition & 1 deletion API/Services/Email/EmailServiceExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static WebApplicationBuilder AddEmailService(this WebApplicationBuilder b

private static WebApplicationBuilder AddSenderContactConfiguration(this WebApplicationBuilder builder)
{
builder.Services.Configure<MailOptions.MailSenderContact>(builder.Configuration.GetRequiredSection(MailOptions.SenderSectionName));
builder.Services.AddSingleton(builder.Configuration.GetRequiredSection(MailOptions.SenderSectionName).Get<MailOptions.MailSenderContact>() ?? throw new NullReferenceException());
return builder;
}
}
10 changes: 5 additions & 5 deletions API/Services/Email/Mailjet/MailjetEmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public sealed class MailjetEmailService : IEmailService, IDisposable
{
private readonly HttpClient _httpClient;
private readonly MailJetOptions _options;
private readonly ILogger<MailjetEmailService> _logger;
private readonly MailOptions.MailSenderContact _sender;
private readonly ILogger<MailjetEmailService> _logger;

/// <summary>
/// DI Constructor
Expand All @@ -23,14 +23,14 @@ public sealed class MailjetEmailService : IEmailService, IDisposable
/// <param name="logger"></param>
public MailjetEmailService(
HttpClient httpClient,
IOptions<MailJetOptions> options,
IOptions<MailOptions.MailSenderContact> sender,
MailJetOptions options,
MailOptions.MailSenderContact sender,
ILogger<MailjetEmailService> logger
)
{
_httpClient = httpClient;
_sender = sender.Value;
_options = options.Value;
_options = options;
_sender = sender;
_logger = logger;
}

Expand Down
3 changes: 3 additions & 0 deletions API/Services/Email/Mailjet/MailjetEmailServiceExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ public static WebApplicationBuilder AddMailjetEmailService(this WebApplicationBu
{
var section = builder.Configuration.GetRequiredSection(MailJetOptions.SectionName);


// TODO Simplify this
builder.Services.Configure<MailJetOptions>(section);
builder.Services.AddSingleton<IValidateOptions<MailJetOptions>, MailJetOptionsValidator>();
builder.Services.AddSingleton<MailJetOptions>(sp => sp.GetRequiredService<IOptions<MailJetOptions>>().Value);

var options = section.Get<MailJetOptions>() ?? throw new NullReferenceException("MailJetOptions is null!");
var basicAuthValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{options.Key}:{options.Secret}"));
Expand Down
8 changes: 4 additions & 4 deletions API/Services/Email/Smtp/SmtpEmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SmtpEmailService> _logger;

private readonly TemplateOptions _templateOptions;
Expand All @@ -24,19 +24,19 @@ public sealed class SmtpEmailService : IEmailService
/// DI Constructor
/// </summary>
/// <param name="templates"></param>
/// <param name="sender"></param>
/// <param name="options"></param>
/// <param name="sender"></param>
/// <param name="logger"></param>
public SmtpEmailService(
SmtpServiceTemplates templates,
IOptions<MailOptions.MailSenderContact> sender,
IOptions<SmtpOptions> options,
MailOptions.MailSenderContact sender,
ILogger<SmtpEmailService> 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
Expand Down
2 changes: 2 additions & 0 deletions API/Services/Email/Smtp/SmtpEmailServiceExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ public static WebApplicationBuilder AddSmtpEmailService(this WebApplicationBuild
{
var section = builder.Configuration.GetRequiredSection(SmtpOptions.SectionName);

// TODO Simplify this
builder.Services.Configure<SmtpOptions>(section);
builder.Services.AddSingleton<IValidateOptions<SmtpOptions>, SmtpOptionsValidator>();
builder.Services.AddSingleton<SmtpOptions>(sp => sp.GetRequiredService<IOptions<SmtpOptions>>().Value);

builder.Services.AddSingleton(new SmtpServiceTemplates
{
Expand Down
4 changes: 2 additions & 2 deletions API/Services/Turnstile/CloudflareTurnstileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ public sealed class CloudflareTurnstileService : ICloudflareTurnstileService
private readonly IHostEnvironment _environment;
private readonly ILogger<CloudflareTurnstileService> _logger;

public CloudflareTurnstileService(HttpClient httpClient, IOptions<TurnstileOptions> options, IHostEnvironment environment, ILogger<CloudflareTurnstileService> logger)
public CloudflareTurnstileService(HttpClient httpClient, TurnstileOptions options, IHostEnvironment environment, ILogger<CloudflareTurnstileService> logger)
{
_httpClient = httpClient;
_options = options.Value;
_options = options;
_environment = environment;
_logger = logger;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ public static WebApplicationBuilder AddCloudflareTurnstileService(this WebApplic
{
var section = builder.Configuration.GetRequiredSection(TurnstileOptions.Turnstile);

// TODO Simplify this
builder.Services.Configure<TurnstileOptions>(section);
builder.Services.AddSingleton<IValidateOptions<TurnstileOptions>, TurnstileOptionsValidator>();
builder.Services.AddSingleton<TurnstileOptions>(sp => sp.GetRequiredService<IOptions<TurnstileOptions>>().Value);

builder.Services.AddHttpClient<ICloudflareTurnstileService, CloudflareTurnstileService>(client =>
{
Expand Down
106 changes: 93 additions & 13 deletions Common/Extensions/ConfigurationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,106 @@
using Microsoft.Extensions.Options;
using OpenShock.Common.Options;
using OpenShock.Common.Options;
using OpenShock.Common.Utils;
using StackExchange.Redis;

namespace OpenShock.Common.Extensions;

public static class ConfigurationExtensions
{
public static WebApplicationBuilder RegisterCommonOpenShockOptions(this WebApplicationBuilder builder)
public static DatabaseOptions RegisterDatabaseOptions(this WebApplicationBuilder builder)
{
if (builder.Environment.IsDevelopment())
var options = builder.Configuration.GetRequiredSection("OpenShock:DB").Get<DatabaseOptions>();
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;
}

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.GetRequiredSection("OpenShock:Redis").Get<RedisSection>();
if (section is null)
throw new InvalidOperationException("Missing or invalid configuration for OpenShock:Redis.");

ConfigurationOptions options;

if (!string.IsNullOrWhiteSpace(section.Conn))
{
Console.WriteLine(builder.Configuration.GetDebugView());
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 InvalidOperationException("Redis Port must be a number between 1 and 65535 (OpenShock:Redis:Port).");
}

options = new ConfigurationOptions
{
User = section.User,
Password = section.Password,
Ssl = false,
};
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.GetSection("OpenShock:Metrics").Get<MetricsOptions>() ?? new MetricsOptions
{
AllowedNetworks = TrustedProxiesFetcher.PrivateNetworks
};

builder.Services.AddSingleton(options);
return options;
}

public static FrontendOptions RegisterFrontendOptions(this WebApplicationBuilder builder)
{
var section = builder.Configuration.GetRequiredSection("OpenShock:Frontend");

builder.Services.Configure<DatabaseOptions>(builder.Configuration.GetRequiredSection(DatabaseOptions.SectionName));
builder.Services.AddSingleton<IValidateOptions<DatabaseOptions>, DatabaseOptionsValidator>();

builder.Services.Configure<RedisOptions>(builder.Configuration.GetRequiredSection(RedisOptions.SectionName));
builder.Services.AddSingleton<IValidateOptions<RedisOptions>, RedisOptionsValidator>();
var options = new FrontendOptions
{
BaseUrl = section.GetValue<Uri>("BaseUrl") ?? throw new InvalidOperationException("Frontend BaseUrl is required (OpenShock:Frontend:BaseUrl)."),
ShortUrl = section.GetValue<Uri>("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.Configure<MetricsOptions>(builder.Configuration.GetSection(MetricsOptions.SectionName));
builder.Services.AddSingleton<IValidateOptions<MetricsOptions>, MetricsOptionsValidator>();
if (options.CookieDomains.Count == 0) throw new InvalidOperationException("At least one cookie domain must be configured (OpenShock:Frontend:CookieDomain).");

return builder;
builder.Services.AddSingleton(options);
return options;

static string[] SplitCsv(string csv)
{
return csv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
}
}
Loading