diff --git a/DysonNetwork.Pass/DysonNetwork.Pass.csproj b/DysonNetwork.Pass/DysonNetwork.Pass.csproj
index 35dd6df..3548976 100644
--- a/DysonNetwork.Pass/DysonNetwork.Pass.csproj
+++ b/DysonNetwork.Pass/DysonNetwork.Pass.csproj
@@ -33,6 +33,8 @@
+
+
diff --git a/DysonNetwork.Pass/Program.cs b/DysonNetwork.Pass/Program.cs
index 666a9c5..9e20295 100644
--- a/DysonNetwork.Pass/Program.cs
+++ b/DysonNetwork.Pass/Program.cs
@@ -1,23 +1,40 @@
+using DysonNetwork.Pass;
+using DysonNetwork.Pass.Startup;
+using Microsoft.EntityFrameworkCore;
+
var builder = WebApplication.CreateBuilder(args);
-// Add services to the container.
+// Configure Kestrel and server options
+builder.ConfigureAppKestrel();
-builder.Services.AddControllers();
-// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
-builder.Services.AddOpenApi();
+// Add metrics and telemetry
+builder.Services.AddAppMetrics();
+
+// Add application services
+builder.Services.AddAppServices(builder.Configuration);
+builder.Services.AddAppRateLimiting();
+builder.Services.AddAppAuthentication();
+builder.Services.AddAppSwagger();
+
+// Add flush handlers and websocket handlers
+builder.Services.AddAppFlushHandlers();
+
+// Add business services
+builder.Services.AddAppBusinessServices(builder.Configuration);
+
+// Add scheduled jobs
+builder.Services.AddAppScheduledJobs();
var app = builder.Build();
-// Configure the HTTP request pipeline.
-if (app.Environment.IsDevelopment())
+// Run database migrations
+using (var scope = app.Services.CreateScope())
{
- app.MapOpenApi();
+ var db = scope.ServiceProvider.GetRequiredService();
+ await db.Database.MigrateAsync();
}
-app.UseHttpsRedirection();
+// Configure application middleware pipeline
+app.ConfigureAppMiddleware(builder.Configuration);
-app.UseAuthorization();
-
-app.MapControllers();
-
-app.Run();
+app.Run();
\ No newline at end of file
diff --git a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs
new file mode 100644
index 0000000..7895bdf
--- /dev/null
+++ b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs
@@ -0,0 +1,66 @@
+using System.Net;
+using DysonNetwork.Pass.Permission;
+using Microsoft.AspNetCore.HttpOverrides;
+using Prometheus;
+
+namespace DysonNetwork.Pass.Startup;
+
+public static class ApplicationConfiguration
+{
+ public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
+ {
+ 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.UseAuthentication();
+ app.UseAuthorization();
+ app.UseMiddleware();
+
+ app.MapControllers().RequireRateLimiting("fixed");
+ app.MapStaticAssets().RequireRateLimiting("fixed");
+ app.MapRazorPages().RequireRateLimiting("fixed");
+
+ 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.Pass/Startup/KestrelConfiguration.cs b/DysonNetwork.Pass/Startup/KestrelConfiguration.cs
new file mode 100644
index 0000000..b042534
--- /dev/null
+++ b/DysonNetwork.Pass/Startup/KestrelConfiguration.cs
@@ -0,0 +1,17 @@
+namespace DysonNetwork.Pass.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.Pass/Startup/MetricsConfiguration.cs b/DysonNetwork.Pass/Startup/MetricsConfiguration.cs
new file mode 100644
index 0000000..78e33cd
--- /dev/null
+++ b/DysonNetwork.Pass/Startup/MetricsConfiguration.cs
@@ -0,0 +1,40 @@
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+using Prometheus;
+using Prometheus.SystemMetrics;
+
+namespace DysonNetwork.Pass.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.Pass/Startup/ScheduledJobsConfiguration.cs b/DysonNetwork.Pass/Startup/ScheduledJobsConfiguration.cs
new file mode 100644
index 0000000..92c7b48
--- /dev/null
+++ b/DysonNetwork.Pass/Startup/ScheduledJobsConfiguration.cs
@@ -0,0 +1,54 @@
+using DysonNetwork.Pass.Handlers;
+using DysonNetwork.Pass.Wallet;
+using Quartz;
+
+namespace DysonNetwork.Pass.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 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 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 subscriptionRenewalJob = new JobKey("SubscriptionRenewal");
+ q.AddJob(opts => opts.WithIdentity(subscriptionRenewalJob));
+ q.AddTrigger(opts => opts
+ .ForJob(subscriptionRenewalJob)
+ .WithIdentity("SubscriptionRenewalTrigger")
+ .WithSimpleSchedule(o => o
+ .WithIntervalInMinutes(30)
+ .RepeatForever())
+ );
+ });
+ services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
+
+ return services;
+ }
+}
diff --git a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..512b18d
--- /dev/null
+++ b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs
@@ -0,0 +1,195 @@
+using System.Globalization;
+using DysonNetwork.Pass.Account;
+using DysonNetwork.Pass.Auth;
+using DysonNetwork.Pass.Auth.OpenId;
+using DysonNetwork.Pass.Email;
+using DysonNetwork.Pass.Localization;
+using DysonNetwork.Pass.Permission;
+using DysonNetwork.Pass.Wallet;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.OpenApi.Models;
+using NodaTime;
+using NodaTime.Serialization.SystemTextJson;
+using StackExchange.Redis;
+using System.Text.Json;
+using System.Threading.RateLimiting;
+using DysonNetwork.Pass.Auth.OidcProvider.Options;
+using DysonNetwork.Pass.Auth.OidcProvider.Services;
+using DysonNetwork.Pass.Handlers;
+using DysonNetwork.Pass.Wallet.PaymentHandlers;
+using DysonNetwork.Shared.Cache;
+using DysonNetwork.Shared.Geo;
+
+namespace DysonNetwork.Pass.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.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 AddAppFlushHandlers(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ 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.Configure(configuration.GetSection("OidcProvider"));
+ services.AddScoped();
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj
index f7c5de9..72aa5d7 100644
--- a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj
+++ b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj
@@ -71,8 +71,8 @@
-
-
+
+