From abc89dc78264ebac9eb785651d7d05195e53a838 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 20 Jun 2025 00:00:40 +0800 Subject: [PATCH] :art: Splitting up startup steps in Program --- DysonNetwork.Sphere/Program.cs | 358 ++---------------- .../Startup/ApplicationConfiguration.cs | 70 ++++ .../Startup/KestrelConfiguration.cs | 17 + .../Startup/MetricsConfiguration.cs | 40 ++ .../Startup/ScheduledJobsConfiguration.cs | 72 ++++ .../Startup/ServiceCollectionExtensions.cs | 227 +++++++++++ 6 files changed, 449 insertions(+), 335 deletions(-) create mode 100644 DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs create mode 100644 DysonNetwork.Sphere/Startup/KestrelConfiguration.cs create mode 100644 DysonNetwork.Sphere/Startup/MetricsConfiguration.cs create mode 100644 DysonNetwork.Sphere/Startup/ScheduledJobsConfiguration.cs create mode 100644 DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs index 192febb..452e629 100644 --- a/DysonNetwork.Sphere/Program.cs +++ b/DysonNetwork.Sphere/Program.cs @@ -1,359 +1,47 @@ -using System.Globalization; -using System.Net; -using System.Text.Json; -using System.Threading.RateLimiting; using DysonNetwork.Sphere; -using DysonNetwork.Sphere.Account; -using DysonNetwork.Sphere.Email; -using DysonNetwork.Sphere.Activity; -using DysonNetwork.Sphere.Auth; -using DysonNetwork.Sphere.Auth.OpenId; -using DysonNetwork.Sphere.Chat; -using DysonNetwork.Sphere.Chat.Realtime; -using DysonNetwork.Sphere.Connection; -using DysonNetwork.Sphere.Connection.Handlers; -using DysonNetwork.Sphere.Localization; -using DysonNetwork.Sphere.Permission; -using DysonNetwork.Sphere.Post; -using DysonNetwork.Sphere.Publisher; -using DysonNetwork.Sphere.Realm; -using DysonNetwork.Sphere.Sticker; -using DysonNetwork.Sphere.Storage; -using DysonNetwork.Sphere.Storage.Handlers; -using DysonNetwork.Sphere.Wallet; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.AspNetCore.RateLimiting; +using DysonNetwork.Sphere.Startup; using Microsoft.EntityFrameworkCore; -using Microsoft.OpenApi.Models; -using NodaTime; -using NodaTime.Serialization.SystemTextJson; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; -using Prometheus; -using Prometheus.SystemMetrics; -using Quartz; -using StackExchange.Redis; -using tusdotnet; using tusdotnet.Stores; var builder = WebApplication.CreateBuilder(args); -builder.Host.UseContentRoot(Directory.GetCurrentDirectory()); -builder.WebHost.ConfigureKestrel(options => -{ - options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; - options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2); - options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30); -}); +// Configure Kestrel and server options +builder.ConfigureAppKestrel(); -// Configure metrics +// Add metrics and telemetry +builder.Services.AddAppMetrics(); -// Prometheus -builder.Services.UseHttpClientMetrics(); -builder.Services.AddHealthChecks(); -builder.Services.AddSystemMetrics(); -builder.Services.AddPrometheusEntityFrameworkMetrics(); -builder.Services.AddPrometheusAspNetCoreMetrics(); -builder.Services.AddPrometheusHttpClientMetrics(); +// Add application services +builder.Services.AddAppServices(builder.Configuration); +builder.Services.AddAppRateLimiting(); +builder.Services.AddAppAuthentication(); +builder.Services.AddAppSwagger(); -// OpenTelemetry -builder.Services.AddOpenTelemetry() - .WithTracing(tracing => - { - tracing - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddOtlpExporter(); - }) - .WithMetrics(metrics => - { - metrics - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation() - .AddOtlpExporter(); - }); +// Add file storage +builder.Services.AddAppFileStorage(builder.Configuration); -// Add services to the container. +// Add flush handlers and websocket handlers +builder.Services.AddAppFlushHandlers(); -builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); - -builder.Services.AddDbContext(); -builder.Services.AddSingleton(_ => -{ - var connection = builder.Configuration.GetConnectionString("FastRetrieve")!; - return ConnectionMultiplexer.Connect(connection); -}); -builder.Services.AddSingleton(SystemClock.Instance); -builder.Services.AddHttpContextAccessor(); -builder.Services.AddSingleton(); - -builder.Services.AddHttpClient(); - -// Register OIDC services -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddControllers().AddJsonOptions(options => -{ - options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; - - options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); -}).AddDataAnnotationsLocalization(options => -{ - options.DataAnnotationLocalizerProvider = (type, factory) => - factory.Create(typeof(SharedResource)); -}); -builder.Services.AddRazorPages(); - -builder.Services.Configure(options => -{ - var supportedCultures = new[] - { - new CultureInfo("en-US"), - new CultureInfo("zh-Hans"), - }; - - options.SupportedCultures = supportedCultures; - options.SupportedUICultures = supportedCultures; -}); - -// Other pipelines - -builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts => -{ - opts.Window = TimeSpan.FromMinutes(1); - opts.PermitLimit = 120; - opts.QueueLimit = 2; - opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; -})); -builder.Services.AddCors(); -builder.Services.AddAuthorization(); -builder.Services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = AuthConstants.SchemeName; - options.DefaultChallengeScheme = AuthConstants.SchemeName; - }) - .AddScheme(AuthConstants.SchemeName, _ => { }); - -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => -{ - options.SwaggerDoc("v1", new OpenApiInfo - { - Version = "v1", - Title = "Solar Network API", - Description = "An open-source social network", - TermsOfService = new Uri("https://solsynth.dev/terms"), - License = new OpenApiLicense - { - Name = "APGLv3", - Url = new Uri("https://www.gnu.org/licenses/agpl-3.0.html") - } - }); - options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme - { - In = ParameterLocation.Header, - Description = "Please enter a valid token", - Name = "Authorization", - Type = SecuritySchemeType.Http, - BearerFormat = "JWT", - Scheme = "Bearer" - }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - } - }, - [] - } - }); -}); -builder.Services.AddOpenApi(); - -var tusStorePath = builder.Configuration.GetSection("Tus").GetValue("StorePath")!; -Directory.CreateDirectory(tusStorePath); -var tusDiskStore = new TusDiskStore(tusStorePath); - -builder.Services.AddSingleton(tusDiskStore); - -builder.Services.AddSingleton(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// The handlers for websocket -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// Services -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.Configure(builder.Configuration.GetSection("GeoIP")); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// Timed task - -builder.Services.AddQuartz(q => -{ - var appDatabaseRecyclingJob = new JobKey("AppDatabaseRecycling"); - q.AddJob(opts => opts.WithIdentity(appDatabaseRecyclingJob)); - q.AddTrigger(opts => opts - .ForJob(appDatabaseRecyclingJob) - .WithIdentity("AppDatabaseRecyclingTrigger") - .WithCronSchedule("0 0 0 * * ?")); - - var cloudFilesRecyclingJob = new JobKey("CloudFilesUnusedRecycling"); - q.AddJob(opts => opts.WithIdentity(cloudFilesRecyclingJob)); - q.AddTrigger(opts => opts - .ForJob(cloudFilesRecyclingJob) - .WithIdentity("CloudFilesUnusedRecyclingTrigger") - .WithSimpleSchedule(o => o.WithIntervalInHours(1).RepeatForever()) - ); - - var actionLogFlushJob = new JobKey("ActionLogFlush"); - q.AddJob(opts => opts.WithIdentity(actionLogFlushJob)); - q.AddTrigger(opts => opts - .ForJob(actionLogFlushJob) - .WithIdentity("ActionLogFlushTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInMinutes(5) - .RepeatForever()) - ); - - var readReceiptFlushJob = new JobKey("ReadReceiptFlush"); - q.AddJob(opts => opts.WithIdentity(readReceiptFlushJob)); - q.AddTrigger(opts => opts - .ForJob(readReceiptFlushJob) - .WithIdentity("ReadReceiptFlushTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInSeconds(60) - .RepeatForever()) - ); - - var lastActiveFlushJob = new JobKey("LastActiveFlush"); - q.AddJob(opts => opts.WithIdentity(lastActiveFlushJob)); - q.AddTrigger(opts => opts - .ForJob(lastActiveFlushJob) - .WithIdentity("LastActiveFlushTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInMinutes(5) - .RepeatForever()) - ); - - var postViewFlushJob = new JobKey("PostViewFlush"); - q.AddJob(opts => opts.WithIdentity(postViewFlushJob)); - q.AddTrigger(opts => opts - .ForJob(postViewFlushJob) - .WithIdentity("PostViewFlushTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInMinutes(1) - .RepeatForever()) - ); -}); -builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); +// Add business services +builder.Services.AddAppBusinessServices(builder.Configuration); +// Add scheduled jobs +builder.Services.AddAppScheduledJobs(); var app = builder.Build(); +// Run database migrations using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); await db.Database.MigrateAsync(); } -app.MapMetrics(); -app.MapOpenApi(); +// Get the TusDiskStore instance +var tusDiskStore = app.Services.GetRequiredService(); -app.UseSwagger(); -app.UseSwaggerUI(); - -app.UseRequestLocalization(); - -// Configure forwarded headers with known proxies from configuration -{ - var knownProxiesSection = builder.Configuration.GetSection("KnownProxies"); - var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All }; - - if (knownProxiesSection.Exists()) - { - var proxyAddresses = knownProxiesSection.Get(); - if (proxyAddresses != null) - foreach (var proxy in proxyAddresses) - if (IPAddress.TryParse(proxy, out var ipAddress)) - forwardedHeadersOptions.KnownProxies.Add(ipAddress); - } - else - { - forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any); - forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any); - } - - app.UseForwardedHeaders(forwardedHeadersOptions); -} - -app.UseCors(opts => - opts.SetIsOriginAllowed(_ => true) - .WithExposedHeaders("*") - .WithHeaders() - .AllowCredentials() - .AllowAnyHeader() - .AllowAnyMethod() -); - -app.UseWebSockets(); -app.UseRateLimiter(); -app.UseHttpsRedirection(); -app.UseAuthorization(); -app.UseMiddleware(); - -app.MapControllers().RequireRateLimiting("fixed"); -app.MapStaticAssets().RequireRateLimiting("fixed"); -app.MapRazorPages().RequireRateLimiting("fixed"); - -app.MapTus("/files/tus", _ => Task.FromResult(TusService.BuildConfiguration(tusDiskStore))); +// Configure application middleware pipeline +app.ConfigureAppMiddleware(builder.Configuration, tusDiskStore); app.Run(); \ No newline at end of file diff --git a/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs b/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs new file mode 100644 index 0000000..8b9fc76 --- /dev/null +++ b/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs @@ -0,0 +1,70 @@ +using System.Net; +using DysonNetwork.Sphere.Permission; +using DysonNetwork.Sphere.Storage; +using Microsoft.AspNetCore.HttpOverrides; +using Prometheus; +using tusdotnet; +using tusdotnet.Stores; + +namespace DysonNetwork.Sphere.Startup; + +public static class ApplicationConfiguration +{ + public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration, TusDiskStore tusDiskStore) + { + app.MapMetrics(); + app.MapOpenApi(); + + app.UseSwagger(); + app.UseSwaggerUI(); + + app.UseRequestLocalization(); + + ConfigureForwardedHeaders(app, configuration); + + app.UseCors(opts => + opts.SetIsOriginAllowed(_ => true) + .WithExposedHeaders("*") + .WithHeaders() + .AllowCredentials() + .AllowAnyHeader() + .AllowAnyMethod() + ); + + app.UseWebSockets(); + app.UseRateLimiter(); + app.UseHttpsRedirection(); + app.UseAuthorization(); + app.UseMiddleware(); + + app.MapControllers().RequireRateLimiting("fixed"); + app.MapStaticAssets().RequireRateLimiting("fixed"); + app.MapRazorPages().RequireRateLimiting("fixed"); + + app.MapTus("/files/tus", _ => Task.FromResult(TusService.BuildConfiguration(tusDiskStore))); + + return app; + } + + private static void ConfigureForwardedHeaders(WebApplication app, IConfiguration configuration) + { + var knownProxiesSection = configuration.GetSection("KnownProxies"); + var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All }; + + if (knownProxiesSection.Exists()) + { + var proxyAddresses = knownProxiesSection.Get(); + if (proxyAddresses != null) + foreach (var proxy in proxyAddresses) + if (IPAddress.TryParse(proxy, out var ipAddress)) + forwardedHeadersOptions.KnownProxies.Add(ipAddress); + } + else + { + forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any); + forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any); + } + + app.UseForwardedHeaders(forwardedHeadersOptions); + } +} diff --git a/DysonNetwork.Sphere/Startup/KestrelConfiguration.cs b/DysonNetwork.Sphere/Startup/KestrelConfiguration.cs new file mode 100644 index 0000000..a73babd --- /dev/null +++ b/DysonNetwork.Sphere/Startup/KestrelConfiguration.cs @@ -0,0 +1,17 @@ +namespace DysonNetwork.Sphere.Startup; + +public static class KestrelConfiguration +{ + public static WebApplicationBuilder ConfigureAppKestrel(this WebApplicationBuilder builder) + { + builder.Host.UseContentRoot(Directory.GetCurrentDirectory()); + builder.WebHost.ConfigureKestrel(options => + { + options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; + options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2); + options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30); + }); + + return builder; + } +} diff --git a/DysonNetwork.Sphere/Startup/MetricsConfiguration.cs b/DysonNetwork.Sphere/Startup/MetricsConfiguration.cs new file mode 100644 index 0000000..ed427da --- /dev/null +++ b/DysonNetwork.Sphere/Startup/MetricsConfiguration.cs @@ -0,0 +1,40 @@ +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using Prometheus; +using Prometheus.SystemMetrics; + +namespace DysonNetwork.Sphere.Startup; + +public static class MetricsConfiguration +{ + public static IServiceCollection AddAppMetrics(this IServiceCollection services) + { + // Prometheus + services.UseHttpClientMetrics(); + services.AddHealthChecks(); + services.AddSystemMetrics(); + services.AddPrometheusEntityFrameworkMetrics(); + services.AddPrometheusAspNetCoreMetrics(); + services.AddPrometheusHttpClientMetrics(); + + // OpenTelemetry + services.AddOpenTelemetry() + .WithTracing(tracing => + { + tracing + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddOtlpExporter(); + }) + .WithMetrics(metrics => + { + metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddOtlpExporter(); + }); + + return services; + } +} diff --git a/DysonNetwork.Sphere/Startup/ScheduledJobsConfiguration.cs b/DysonNetwork.Sphere/Startup/ScheduledJobsConfiguration.cs new file mode 100644 index 0000000..3a5c6fc --- /dev/null +++ b/DysonNetwork.Sphere/Startup/ScheduledJobsConfiguration.cs @@ -0,0 +1,72 @@ +using DysonNetwork.Sphere.Storage; +using DysonNetwork.Sphere.Storage.Handlers; +using Quartz; + +namespace DysonNetwork.Sphere.Startup; + +public static class ScheduledJobsConfiguration +{ + public static IServiceCollection AddAppScheduledJobs(this IServiceCollection services) + { + services.AddQuartz(q => + { + var appDatabaseRecyclingJob = new JobKey("AppDatabaseRecycling"); + q.AddJob(opts => opts.WithIdentity(appDatabaseRecyclingJob)); + q.AddTrigger(opts => opts + .ForJob(appDatabaseRecyclingJob) + .WithIdentity("AppDatabaseRecyclingTrigger") + .WithCronSchedule("0 0 0 * * ?")); + + var cloudFilesRecyclingJob = new JobKey("CloudFilesUnusedRecycling"); + q.AddJob(opts => opts.WithIdentity(cloudFilesRecyclingJob)); + q.AddTrigger(opts => opts + .ForJob(cloudFilesRecyclingJob) + .WithIdentity("CloudFilesUnusedRecyclingTrigger") + .WithSimpleSchedule(o => o.WithIntervalInHours(1).RepeatForever()) + ); + + var actionLogFlushJob = new JobKey("ActionLogFlush"); + q.AddJob(opts => opts.WithIdentity(actionLogFlushJob)); + q.AddTrigger(opts => opts + .ForJob(actionLogFlushJob) + .WithIdentity("ActionLogFlushTrigger") + .WithSimpleSchedule(o => o + .WithIntervalInMinutes(5) + .RepeatForever()) + ); + + var readReceiptFlushJob = new JobKey("ReadReceiptFlush"); + q.AddJob(opts => opts.WithIdentity(readReceiptFlushJob)); + q.AddTrigger(opts => opts + .ForJob(readReceiptFlushJob) + .WithIdentity("ReadReceiptFlushTrigger") + .WithSimpleSchedule(o => o + .WithIntervalInSeconds(60) + .RepeatForever()) + ); + + var lastActiveFlushJob = new JobKey("LastActiveFlush"); + q.AddJob(opts => opts.WithIdentity(lastActiveFlushJob)); + q.AddTrigger(opts => opts + .ForJob(lastActiveFlushJob) + .WithIdentity("LastActiveFlushTrigger") + .WithSimpleSchedule(o => o + .WithIntervalInMinutes(5) + .RepeatForever()) + ); + + var postViewFlushJob = new JobKey("PostViewFlush"); + q.AddJob(opts => opts.WithIdentity(postViewFlushJob)); + q.AddTrigger(opts => opts + .ForJob(postViewFlushJob) + .WithIdentity("PostViewFlushTrigger") + .WithSimpleSchedule(o => o + .WithIntervalInMinutes(1) + .RepeatForever()) + ); + }); + services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); + + return services; + } +} diff --git a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..4ff96b5 --- /dev/null +++ b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs @@ -0,0 +1,227 @@ +using System.Globalization; +using DysonNetwork.Sphere.Account; +using DysonNetwork.Sphere.Activity; +using DysonNetwork.Sphere.Auth; +using DysonNetwork.Sphere.Auth.OpenId; +using DysonNetwork.Sphere.Chat; +using DysonNetwork.Sphere.Chat.Realtime; +using DysonNetwork.Sphere.Connection; +using DysonNetwork.Sphere.Connection.Handlers; +using DysonNetwork.Sphere.Email; +using DysonNetwork.Sphere.Localization; +using DysonNetwork.Sphere.Permission; +using DysonNetwork.Sphere.Post; +using DysonNetwork.Sphere.Publisher; +using DysonNetwork.Sphere.Realm; +using DysonNetwork.Sphere.Sticker; +using DysonNetwork.Sphere.Storage; +using DysonNetwork.Sphere.Storage.Handlers; +using DysonNetwork.Sphere.Wallet; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; +using NodaTime; +using NodaTime.Serialization.SystemTextJson; +using Quartz; +using StackExchange.Redis; +using System.Text.Json; +using System.Threading.RateLimiting; +using tusdotnet.Stores; + +namespace DysonNetwork.Sphere.Startup; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddLocalization(options => options.ResourcesPath = "Resources"); + + services.AddDbContext(); + services.AddSingleton(_ => + { + var connection = configuration.GetConnectionString("FastRetrieve")!; + return ConnectionMultiplexer.Connect(connection); + }); + services.AddSingleton(SystemClock.Instance); + services.AddHttpContextAccessor(); + services.AddSingleton(); + + services.AddHttpClient(); + + // Register OIDC services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddControllers().AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; + + options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + }).AddDataAnnotationsLocalization(options => + { + options.DataAnnotationLocalizerProvider = (type, factory) => + factory.Create(typeof(SharedResource)); + }); + services.AddRazorPages(); + + services.Configure(options => + { + var supportedCultures = new[] + { + new CultureInfo("en-US"), + new CultureInfo("zh-Hans"), + }; + + options.SupportedCultures = supportedCultures; + options.SupportedUICultures = supportedCultures; + }); + + return services; + } + + public static IServiceCollection AddAppRateLimiting(this IServiceCollection services) + { + services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts => + { + opts.Window = TimeSpan.FromMinutes(1); + opts.PermitLimit = 120; + opts.QueueLimit = 2; + opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + })); + + return services; + } + + public static IServiceCollection AddAppAuthentication(this IServiceCollection services) + { + services.AddCors(); + services.AddAuthorization(); + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = AuthConstants.SchemeName; + options.DefaultChallengeScheme = AuthConstants.SchemeName; + }) + .AddScheme(AuthConstants.SchemeName, _ => { }); + + return services; + } + + public static IServiceCollection AddAppSwagger(this IServiceCollection services) + { + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "Solar Network API", + Description = "An open-source social network", + TermsOfService = new Uri("https://solsynth.dev/terms"), + License = new OpenApiLicense + { + Name = "APGLv3", + Url = new Uri("https://www.gnu.org/licenses/agpl-3.0.html") + } + }); + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter a valid token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + [] + } + }); + }); + services.AddOpenApi(); + + return services; + } + + public static IServiceCollection AddAppFileStorage(this IServiceCollection services, IConfiguration configuration) + { + var tusStorePath = configuration.GetSection("Tus").GetValue("StorePath")!; + Directory.CreateDirectory(tusStorePath); + var tusDiskStore = new TusDiskStore(tusStorePath); + + services.AddSingleton(tusDiskStore); + + return services; + } + + public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services) + { + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // The handlers for websocket + services.AddScoped(); + services.AddScoped(); + + return services; + } + + public static IServiceCollection AddAppBusinessServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + services.AddScoped(); + services.Configure(configuration.GetSection("GeoIP")); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } +}