using System.Text; using System.Threading.RateLimiting; using LehrerApp.Api; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.RateLimiting; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); builder.WebHost.UseKestrel(o => { var port = builder.Configuration.GetValue("Api:Port", 5000); o.ListenAnyIP(port); // Anhänge sind clientseitig auf 10 MB begrenzt (IAttachmentStorage.MaxSizeBytes) - etwas // Puffer für Verschlüsselungs-Overhead/JSON-Ereignisse, aber trotzdem eine harte Obergrenze. o.Limits.MaxRequestBodySize = 15 * 1024 * 1024; }); var data = builder.Configuration["Api:DataPath"] ?? "./data"; if (args.Length > 0 && args[0] == "create-user") return await Cli.RunCreateUserAsync(data, args); if (args.Length > 0 && args[0] == "set-password") return await Cli.RunSetPasswordAsync(data, args); var secret = builder.Configuration["JWT_SECRET"] ?? throw new InvalidOperationException("JWT_SECRET nicht konfiguriert."); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(o => o.TokenValidationParameters = new() { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)), ValidateIssuer = false, ValidateAudience = false, ClockSkew = TimeSpan.FromMinutes(5), }); builder.Services.AddAuthorization(); // Läuft hinter einem Reverse Proxy (Dokploy/Traefik) - ohne diese Weiterleitung sähe Kestrel für // JEDE Anfrage dieselbe interne Proxy-IP als Connection.RemoteIpAddress, wodurch das unten // definierte Pro-IP-Rate-Limit faktisch zu einem einzigen globalen Limit für die gesamte // Bereitstellung würde (Bug, siehe TODO.md 10.2.2 Nachtrag). KnownNetworks/KnownProxies bewusst // geleert: der Container ist nur über den Reverse Proxy erreichbar, nie direkt aus dem Internet, // daher ist der unmittelbare Absender von X-Forwarded-For hier immer vertrauenswürdig. builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.KnownIPNetworks.Clear(); options.KnownProxies.Clear(); }); // Rate Limiting (10.2.2): striktes Limit gezielt gegen Brute-Force auf /api/auth/login, plus ein // grobes globales Limit pro IP als einfacher Schutz vor Überlastung der übrigen Endpunkte. builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddFixedWindowLimiter("login", o => { o.PermitLimit = 5; o.Window = TimeSpan.FromMinutes(1); o.QueueLimit = 0; }); options.GlobalLimiter = PartitionedRateLimiter.Create(context => RateLimitPartition.GetFixedWindowLimiter( partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown", factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 120, Window = TimeSpan.FromMinutes(1), QueueLimit = 0, })); }); builder.Services.AddSingleton(_ => new UserStore(data)); builder.Services.AddSingleton(_ => new AttachmentStore(data)); builder.Services.AddSingleton(_ => new EventStore(data)); builder.Services.AddSingleton(_ => new SnapshotStore(data)); builder.Services.AddSingleton(_ => new ReadableSnapshotStore(data)); builder.Services.AddSingleton(sp => new PlainEventStore(sp.GetRequiredService())); var app = builder.Build(); app.UseForwardedHeaders(); app.UseRateLimiter(); app.UseAuthentication(); app.UseAuthorization(); app.MapAuthEndpoints(secret); app.MapSyncEndpoints(); app.MapAttachmentEndpoints(); app.MapSnapshotEndpoints(); app.MapReadableSnapshotEndpoints(); app.MapPlainSyncEndpoints(); app.Run(); return 0;