src/Program.cs
Rama main · réplica de lectura del repositorio original.
using Aptabase.Data;
using Aptabase.Data.Migrations;
using Aptabase.Features;
using Aptabase.Features.Apps;
using Aptabase.Features.Authentication;
using Aptabase.Features.Billing;
using Aptabase.Features.Billing.LemonSqueezy;
using Aptabase.Features.Blob;
using Aptabase.Features.ErrorReporting;
using Aptabase.Features.ErrorReporting.Buffer;
using Aptabase.Features.GeoIP;
using Aptabase.Features.Ingestion;
using Aptabase.Features.Ingestion.Buffer;
using Aptabase.Features.Notification;
using Aptabase.Features.Privacy;
using Aptabase.Features.Stats;
using ClickHouse.Client.ADO;
using Features.Cache;
using FluentMigrator.Runner;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Hosting;
using System.Net.Http.Headers;
using System.Threading.RateLimiting;
public partial class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.AddServerHeader = false;
// Set to 1/4 of the default value to better support mobile devices on slow networks
options.Limits.MinRequestBodyDataRate = new MinDataRate(bytesPerSecond: 60, gracePeriod: TimeSpan.FromSeconds(10));
});
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
var appEnv = EnvSettings.Load();
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
});
builder.Services.AddCors(options =>
{
options.AddPolicy(name: "AllowAptabaseCom", policy =>
{
policy.WithOrigins("https://aptabase.com")
.WithMethods("GET")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromHours(1));
});
options.AddPolicy(name: "AllowAny", policy =>
{
policy.AllowAnyOrigin()
.WithHeaders("content-type", "app-key")
.WithMethods("POST", "OPTIONS")
.SetPreflightMaxAge(TimeSpan.FromHours(1));
});
});
if (appEnv.IsManagedCloud)
{
builder.Services.AddDataProtection()
.PersistKeysToAWSSystemsManager("/aptabase/production/aspnet-dataprotection/");
}
builder.Services.AddControllers();
builder.Services.AddResponseCaching();
builder.Services.AddMemoryCache();
builder.Services.AddHttpContextAccessor();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(365);
options.Cookie.Name = "auth-session";
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.MaxAge = TimeSpan.FromDays(365);
}).AddGitHub(appEnv).AddGoogle(appEnv);
builder.Services.AddRateLimiter(c =>
{
c.RejectionStatusCode = 429;
c.OnRejected = (ctx, _) =>
{
var http = ctx.HttpContext;
http.RequestServices.GetRequiredService<IRejectionStats>().Record(
"rate_limit",
http.Request.Headers["App-Key"],
http.ResolveClientIpAddress(),
http.Request.Headers.UserAgent);
return ValueTask.CompletedTask;
};
c.AddPolicy("SignUp", httpContext => RateLimitPartition.GetFixedWindowLimiter(
httpContext.ResolveClientIpAddress(),
partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 4,
Window = TimeSpan.FromHours(1)
})
);
c.AddPolicy("Stats", httpContext => RateLimitPartition.GetFixedWindowLimiter(
httpContext.ResolveClientIpAddress(),
partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 1000,
Window = TimeSpan.FromHours(1)
})
);
c.AddPolicy("EventIngestion", httpContext => RateLimitPartition.GetFixedWindowLimiter(
httpContext.ResolveClientIpAddress(),
partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 20,
Window = TimeSpan.FromSeconds(1)
})
);
c.AddPolicy("FeatureFlags", httpContext => RateLimitPartition.GetFixedWindowLimiter(
httpContext.ResolveClientIpAddress(),
partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 10,
Window = TimeSpan.FromSeconds(1)
})
);
});
builder.Services.AddSingleton(appEnv);
builder.Services.AddHealthChecks();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddSingleton<ICache, DatabaseCache>();
builder.Services.AddSingleton<IAppQueries, AppQueries>();
builder.Services.AddSingleton<IBillingQueries, BillingQueries>();
builder.Services.AddSingleton<IPrivacyQueries, PrivacyQueries>();
builder.Services.AddSingleton<IUserHasher, DailyUserHasher>();
builder.Services.AddSingleton<IAuthTokenManager, AuthTokenManager>();
builder.Services.AddSingleton<IIngestionCache, IngestionCache>();
builder.Services.AddSingleton<IRejectionStats, RejectionStats>();
builder.Services.AddHostedService<RejectionStatsReporter>();
builder.Services.AddSingleton<IBlobService, DatabaseBlobService>();
builder.Services.AddSingleton<IEventBuffer, InMemoryEventBuffer>();
builder.Services.AddSingleton<IErrorBuffer, InMemoryErrorBuffer>();
builder.Services.AddSingleton<IPiiSanitizer, PiiSanitizer>();
builder.Services.AddHostedService<EventBackgroundWritter>();
builder.Services.AddHostedService<ErrorBackgroundWritter>();
if (appEnv.ErrorQuotaEnabled)
builder.Services.AddHostedService<ResetErrorCountCronJob>();
builder.Services.AddHostedService<PurgeDailySaltsCronJob>();
if (appEnv.IsBillingEnabled)
{
builder.Services.AddHostedService<OveruseNotificationCronJob>();
builder.Services.AddHostedService<TrialExpiredCronJob>();
builder.Services.AddHostedService<TrialNotificationCronJob>();
builder.Services.AddHostedService<ResetOveruseCronJob>();
}
builder.Services.AddGeoIPClient(appEnv);
builder.Services.AddEmailClient(appEnv);
builder.Services.AddLemonSqueezy(appEnv);
if (!string.IsNullOrEmpty(appEnv.ClickHouseConnectionString))
{
builder.Services.AddSingleton(x => new ClickHouseConnection(appEnv.ClickHouseConnectionString));
builder.Services.AddSingleton<IClickHouseMigrationRunner, ClickHouseMigrationRunner>();
builder.Services.AddSingleton<IQueryClient, ClickHouseQueryClient>();
builder.Services.AddSingleton<IIngestionClient, ClickHouseIngestionClient>();
builder.Services.AddSingleton<IErrorIngestionClient, ClickHouseErrorIngestionClient>();
builder.Services.AddSingleton<IErrorQueryClient, ClickHouseErrorQueryClient>();
}
else
{
builder.Services.AddSingleton<IQueryClient, TinybirdQueryClient>();
builder.Services.AddSingleton<IIngestionClient, TinybirdIngestionClient>();
builder.Services.AddSingleton<IErrorIngestionClient, TinybirdErrorIngestionClient>();
builder.Services.AddSingleton<IErrorQueryClient, TinybirdErrorQueryClient>();
builder.Services.AddHttpClient("Tinybird", client =>
{
client.BaseAddress = new Uri(appEnv.TinybirdBaseUrl);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", appEnv.TinybirdToken);
});
}
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
builder.Services.AddSingleton<IDbContext, DbContext>();
builder.Services.AddNpgsqlDataSource(appEnv.ConnectionString);
builder.Services.AddFluentMigratorCore().ConfigureRunner(
r => r.AddPostgres()
.WithGlobalConnectionString(appEnv.ConnectionString)
.WithVersionTable(new VersionTable())
.ScanIn(typeof(Program).Assembly).For.Migrations()
);
if (appEnv.IsDevelopment)
{
builder.AddServiceDefaults();
}
var app = builder.Build();
app.UseResponseCompression();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
// App may be running on HTTP behind a HTTPS proxy/load balancer
// So we need to use the X-Forwarded-Proto forwarded header
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedFor
});
if (appEnv.IsProduction)
{
app.Use(async (context, next) =>
{
context.Request.Scheme = "https";
await next();
});
}
app.MapHealthChecks("/healthz");
app.UseMiddleware<ExceptionMiddleware>();
app.UseRateLimiter();
app.UseCors();
app.UseAuthentication();
app.MapControllers();
app.UseResponseCaching();
if (appEnv.IsProduction)
{
app.UseHsts();
app.MapFallbackToFile("index.html", new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append("Content-Security-Policy", "default-src 'self' 'unsafe-inline'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline' https://client.crisp.chat; script-src 'self' 'unsafe-inline' https://client.crisp.chat; font-src 'self' https://client.crisp.chat; connect-src 'self' https://raw.githubusercontent.com wss://client.relay.crisp.chat https://client.crisp.chat;");
ctx.Context.Response.Headers.Append("X-Frame-Options", "DENY");
ctx.Context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
ctx.Context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
ctx.Context.Response.Headers.Append("Cache-Control", "no-store,no-cache");
}
});
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
if (ctx.Context.Request.Path.StartsWithSegments("/assets"))
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=31536000,immutable");
}
});
}
RunMigrations(app.Services);
app.Run();
}
public static void RunMigrations(IServiceProvider sp)
{
using var scope = sp.CreateScope();
// Execute Postgres migrations
var runner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();
runner.MigrateUp();
var env = scope.ServiceProvider.GetRequiredService<EnvSettings>();
if (!string.IsNullOrEmpty(env.ClickHouseConnectionString))
{
// Execute ClickHouse migrations (if applicable)
var chRunner = scope.ServiceProvider.GetRequiredService<IClickHouseMigrationRunner>();
chRunner.MigrateUp();
}
}
}