using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using SameBeat.Api.Hubs; using SameBeat.Api.Konfiguracja; using SameBeat.Api.Middleware; using SameBeat.Application.Interfejsy; using SameBeat.Infrastructure.BazaDanych; using SameBeat.Infrastructure.Logowanie; using Serilog; /// Nazwa polityki rate limitingu dla /api/auth/login -- osobna, surowsza niz reszta /api/* (ETAP 7). const string PolitykaLogowania = "logowanie"; const string NazwaCiasteczkaAuth = "sb_auth"; const string NazwaPolitykiCors = "PolitykaFrontendu"; var builder = WebApplication.CreateBuilder(args); // ETAP 7 -- limit rozmiaru body requestu (ochrona przed nadmiernie duzymi payloadami; ta aplikacja // nie ma zadnego endpointu na upload plikow -- wszystkie DTO to krotkie JSONy, wiec 32KB jest sowitym // zapasem). Kestrel to jedyny serwer tej aplikacji (Sdk.Web, bez IIS/Nginx w procesie) -- limit tu // wystarczy, nie trzeba go osobno powtarzac np. w FormOptions (nie uzywamy multipart/form-data). builder.WebHost.ConfigureKestrel(opcje => { opcje.Limits.MaxRequestBodySize = 32 * 1024; }); // Serilog jest logerem calej aplikacji od pierwszej linii (rowniez bledow startu, np. zlej // konfiguracji polaczenia z baza) -- konfiguracja w Infrastructure/Logowanie, katalog logow ponizej. var katalogLogow = Path.Combine(builder.Environment.ContentRootPath, "logs"); Directory.CreateDirectory(katalogLogow); Log.Logger = KonfiguracjaSerilog.Zbuduj(builder.Configuration, katalogLogow).CreateLogger(); builder.Host.UseSerilog(); builder.Services.DodajUslugiSameBeat(builder.Configuration); builder.Services .AddControllers(opcje => opcje.Conventions.Add(new KonwencjaAutoryzacjiHosta())) .AddJsonOptions(opcje => opcje.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(opcje => { opcje.SwaggerDoc("v1", new OpenApiInfo { Title = "SameBeat API", Version = "v1" }); }); builder.Services.AddSignalR(); // ETAP 8 -- licznik otwartych polaczen SignalR w pamieci procesu (patrz Api/Monitoring/LicznikPolaczenSignalR.cs), // singleton bo ma zyc caly czas dzialania procesu, wspoldzielony miedzy StanImprezyHub (aktualizacja) // a HealthController/HostDiagnostykaController (odczyt). builder.Services.AddSingleton(); var ustawieniaJwtSekcja = builder.Configuration.GetSection("Jwt"); builder.Services .AddAuthentication(opcje => { opcje.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; opcje.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(opcje => { var sekret = ustawieniaJwtSekcja["Sekret"]; opcje.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = ustawieniaJwtSekcja["Wydawca"], ValidateAudience = true, ValidAudience = ustawieniaJwtSekcja["Odbiorca"], ValidateIssuerSigningKey = true, IssuerSigningKey = string.IsNullOrWhiteSpace(sekret) ? null : new SymmetricSecurityKey(Encoding.UTF8.GetBytes(sekret)), ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30) }; // Token JWT siedzi w httpOnly cookie, nie w naglowku Authorization -- trzeba go stamtad // recznie wyciagnac, inaczej middleware autoryzacji nigdy go nie znajdzie. opcje.Events = new JwtBearerEvents { OnMessageReceived = kontekst => { if (kontekst.Request.Cookies.TryGetValue(NazwaCiasteczkaAuth, out var tokenZCiasteczka)) { kontekst.Token = tokenZCiasteczka; } return Task.CompletedTask; } }; }); builder.Services.AddAuthorization(); var dozwoloneOrigin = builder.Configuration["Cors:DozwoloneOrigin"] ?? "http://localhost:5173"; builder.Services.AddCors(opcje => { opcje.AddPolicy(NazwaPolitykiCors, polityka => polityka.WithOrigins(dozwoloneOrigin) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials()); }); // ETAP 7 -- rate limiting REST API (Microsoft.AspNetCore.RateLimiting, wbudowane w .NET 8, bez NuGet). // Klucz partycji to adres IP wywolujacego -- to samo IP moze bic zarowno w rozne endpointy /api/* // (limit ogolny, GlobalLimiter ponizej) jak i specyficznie w /api/auth/login (osobna, surowsza // polityka "logowanie" -- ochrona przed brute force, patrz [EnableRateLimiting] w AuthController). // UWAGA: polityka nalozona przez [EnableRateLimiting] na endpoincie CALKOWICIE ZASTEPUJE GlobalLimiter // dla tego endpointu (nie sumuja sie) -- login jest wiec liczony WYLACZNIE wg polityki "logowania". builder.Services.AddRateLimiter(opcje => { opcje.RejectionStatusCode = StatusCodes.Status429TooManyRequests; opcje.OnRejected = async (kontekstOdrzucenia, token) => { kontekstOdrzucenia.HttpContext.Response.ContentType = "application/json"; await kontekstOdrzucenia.HttpContext.Response.WriteAsync( JsonSerializer.Serialize(new { blad = "Zbyt wiele zadan z tego adresu. Sprobuj ponownie za chwile." }), token); }; // Polityka ogolna -- WYLACZNIE dla /api/* (Swagger, /hub/impreza itd. nie sa nia objete, zeby nie // dolozyc limitu do negocjacji SignalR, ktora i tak jest chroniona osobno przez autoryzacje/CORS). opcje.GlobalLimiter = PartitionedRateLimiter.Create(kontekst => { if (!kontekst.Request.Path.StartsWithSegments("/api")) { return RateLimitPartition.GetNoLimiter("poza-api"); } return RateLimitPartition.GetSlidingWindowLimiter(PobierzKluczIp(kontekst), _ => new SlidingWindowRateLimiterOptions { PermitLimit = 60, Window = TimeSpan.FromMinutes(1), SegmentsPerWindow = 6, QueueLimit = 0 }); }); // Surowsza polityka logowania -- 5 prob / 5 minut per IP, NIEZALEZNA od blokady per-konto // w SerwisLogowania (dwie warstwy, patrz komentarz tam). opcje.AddPolicy(PolitykaLogowania, kontekst => RateLimitPartition.GetSlidingWindowLimiter(PobierzKluczIp(kontekst), _ => new SlidingWindowRateLimiterOptions { PermitLimit = 5, Window = TimeSpan.FromMinutes(5), SegmentsPerWindow = 5, QueueLimit = 0 })); }); var app = builder.Build(); // Globalny lapacz wyjatkow na samym poczatku potoku -- zeby zlapac rowniez bledy z pozniejszych // middleware (autoryzacja, routing), nie tylko z kontrolerow. app.UseMiddleware(); // ETAP 7 -- naglowki bezpieczenstwa (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy) // na KAZDEJ odpowiedzi, wiec zaraz po lapaczu wyjatkow -- patrz komentarz w tej klasie. app.UseMiddleware(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); // Seed konta hosta dev + proba nalozenia migracji dziala tylko lokalnie i tylko jesli baza jest // osiagalna -- na swiezym srodowisku bez postawionego Postgresa API ma sie mimo to normalnie uruchomic. using var zakresStartu = app.Services.CreateScope(); var logerStartu = zakresStartu.ServiceProvider.GetRequiredService>(); try { var kontekst = zakresStartu.ServiceProvider.GetRequiredService(); await kontekst.Database.MigrateAsync(); var uslugaHaszowaniaHasel = zakresStartu.ServiceProvider.GetRequiredService(); await SeedDanych.SeedujKontoHostaDevAsync(kontekst, uslugaHaszowaniaHasel, builder.Configuration, logerStartu); await SeedDanych.SeedujWydarzenieDevAsync(kontekst, logerStartu); } catch (Exception wyjatek) { logerStartu.LogWarning(wyjatek, "Nie udalo sie zastosowac migracji / zasiac konta dev -- baza danych jest prawdopodobnie niedostepna. " + "API mimo to startuje; uzupelnij ConnectionStrings:BazaDanych, gdy baza bedzie gotowa."); } } app.UseHttpsRedirection(); app.UseCors(NazwaPolitykiCors); // UseRateLimiter PO UseCors -- zeby odpowiedz 429 (rowniez ta z GlobalLimiter/politykowej blokady) // nadal miala naglowki CORS i przegladarka mogla ja w ogole odczytac zamiast zgloszic blad CORS // zamiast prawdziwego bledu 429. app.UseRateLimiter(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapHub("/hub/impreza"); try { Log.Information("Start SameBeat.Api."); app.Run(); } finally { Log.CloseAndFlush(); } /// /// Klucz partycji rate limitera -- adres IP wywolujacego. RemoteIpAddress bierze pod uwage /// UseForwardedHeaders, gdyby kiedys aplikacja stanela za reverse proxy/load balancerem (na razie /// nie jest skonfigurowany -- do uzupelnienia razem z prawdziwym adresem produkcyjnym, patrz appsettings). /// static string PobierzKluczIp(HttpContext kontekst) => kontekst.Connection.RemoteIpAddress?.ToString() ?? "nieznany-adres-ip"; public partial class Program { // Pusta, celowo publiczna czesciowa deklaracja -- pozwala WebApplicationFactory<Program> // (SameBeat.Tests/Integration) uruchomic te aplikacje w testach. Bez tego klasa Program // wygenerowana z top-level statements jest internal i niewidoczna dla projektu testowego. }