Files
LehrerApp/LehrerApp.Api/Program.cs
T
adminandClaude Sonnet 5 6774123270 Baustein 10: Deployment-Haertung (Kapitel 10)
- Rate Limiting ueber ASP.NET Cores eingebautes
  Microsoft.AspNetCore.RateLimiting (keine neue Paketabhaengigkeit):
  /api/auth/login auf 5 Versuche/Minute begrenzt (Brute-Force-Schutz),
  alle Endpunkte zusaetzlich global auf 120 Anfragen/Minute je IP
- Kestrel MaxRequestBodySize auf 15 MB gedeckelt (Anhaenge sind
  clientseitig ohnehin auf 10 MB begrenzt)
- Neu docker/backup.sh: Tar-Archiv von ./data (Ereignis-Logs,
  Snapshots, Anhaenge, Nutzer), raeumt Archive aelter als 30 Tage auf,
  laeuft direkt auf dem Host
- docker/README.md um Backup- und Rate-Limit-Dokumentation ergaenzt

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 11:38:42 +02:00

81 lines
3.0 KiB
C#

using System.Text;
using System.Threading.RateLimiting;
using LehrerApp.Api;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseKestrel(o =>
{
var port = builder.Configuration.GetValue<int>("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);
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();
// 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<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 120,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0,
}));
});
builder.Services.AddSingleton<UserStore>(_ => new UserStore(data));
builder.Services.AddSingleton<AttachmentStore>(_ => new AttachmentStore(data));
builder.Services.AddSingleton<EventStore>(_ => new EventStore(data));
builder.Services.AddSingleton<SnapshotStore>(_ => new SnapshotStore(data));
builder.Services.AddSingleton<ReadableSnapshotStore>(_ => new ReadableSnapshotStore(data));
builder.Services.AddSingleton<PlainEventStore>(sp =>
new PlainEventStore(sp.GetRequiredService<EventStore>()));
var app = builder.Build();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapAuthEndpoints(secret);
app.MapSyncEndpoints();
app.MapAttachmentEndpoints();
app.MapSnapshotEndpoints();
app.MapReadableSnapshotEndpoints();
app.MapPlainSyncEndpoints();
app.Run();
return 0;