using AutoBidder.Services; using AutoBidder.Data; using AutoBidder.Models; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.DataProtection; using System.Data.Common; var builder = WebApplication.CreateBuilder(args); // FORCE ASPNETCORE_URLS to prevent any override // Questo garantisce che il container ascolti SEMPRE sulla porta configurata if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_URLS"))) { builder.WebHost.UseUrls("http://+:8080"); } else { builder.WebHost.UseUrls(Environment.GetEnvironmentVariable("ASPNETCORE_URLS")!); } // Configura Kestrel solo per HTTPS opzionale // HTTP è gestito da ASPNETCORE_URLS (default: http://+:8080 nel Dockerfile) var enableHttps = builder.Configuration.GetValue("Kestrel:EnableHttps", false); if (enableHttps) { builder.WebHost.ConfigureKestrel(options => { try { // In produzione, cerca il certificato da configurazione var certPath = builder.Configuration["Kestrel:Certificates:Default:Path"]; var certPassword = builder.Configuration["Kestrel:Certificates:Default:Password"]; if (!string.IsNullOrEmpty(certPath) && File.Exists(certPath)) { options.ListenAnyIP(8443, listenOptions => { listenOptions.UseHttps(certPath, certPassword); Console.WriteLine($"[Kestrel] HTTPS enabled with certificate: {certPath}"); }); } else if (builder.Environment.IsDevelopment()) { // Certificato di sviluppo SOLO in ambiente Development options.ListenAnyIP(5001, listenOptions => { listenOptions.UseHttps(); Console.WriteLine("[Kestrel] HTTPS enabled with development certificate"); }); } else { Console.WriteLine("[Kestrel] HTTPS requested but no certificate found"); Console.WriteLine("[Kestrel] Running in HTTP-only mode"); } } catch (Exception ex) { Console.WriteLine($"[Kestrel] Failed to enable HTTPS: {ex.Message}"); Console.WriteLine("[Kestrel] Running in HTTP-only mode"); } }); } else { Console.WriteLine("[Kestrel] HTTPS disabled - running in HTTP-only mode"); Console.WriteLine("[Kestrel] Use a reverse proxy (nginx/traefik) for SSL termination"); Console.WriteLine($"[Kestrel] Listening on: {Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? "http://+:8080"}"); } // Add services to the container builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); // Configura Data Protection per evitare CryptographicException var dataProtectionPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AutoBidder", "DataProtection-Keys" ); if (!Directory.Exists(dataProtectionPath)) { Directory.CreateDirectory(dataProtectionPath); } builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(dataProtectionPath)) .SetApplicationName("AutoBidder"); // ============================================ // CONFIGURAZIONE AUTENTICAZIONE E SICUREZZA // ============================================ // Database per Identity (SQLite) var identityDbPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AutoBidder", "identity.db" ); builder.Services.AddDbContext(options => { options.UseSqlite($"Data Source={identityDbPath}"); }); // ASP.NET Core Identity builder.Services.AddIdentity(options => { // Password settings (SICUREZZA FORTE) options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireUppercase = true; options.Password.RequireNonAlphanumeric = true; options.Password.RequiredLength = 12; options.Password.RequiredUniqueChars = 4; // Lockout settings (protezione brute-force) options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = true; // User settings options.User.RequireUniqueEmail = false; options.SignIn.RequireConfirmedAccount = false; }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); // Cookie configuration (SICUREZZA TAILSCALE) builder.Services.ConfigureApplicationCookie(options => { options.Cookie.Name = "AutoBidder.Auth"; options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // HTTP su Tailscale OK options.Cookie.SameSite = SameSiteMode.Lax; options.ExpireTimeSpan = TimeSpan.FromDays(7); options.SlidingExpiration = true; // Redirect per autenticazione (Razor Pages) options.LoginPath = "/Account/Login"; options.LogoutPath = "/Account/Logout"; options.AccessDeniedPath = "/Account/Login"; }); // Authorization builder.Services.AddAuthorization(); builder.Services.AddCascadingAuthenticationState(); // Configura HTTPS Redirection per produzione if (!builder.Environment.IsDevelopment()) { builder.Services.AddHsts(options => { options.MaxAge = TimeSpan.FromDays(365); options.IncludeSubDomains = true; options.Preload = true; }); } // Configura Database SQLite per statistiche (fallback locale) builder.Services.AddDbContext(options => { var dbPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AutoBidder", "statistics.db" ); // Crea directory se non esiste var directory = Path.GetDirectoryName(dbPath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } options.UseSqlite($"Data Source={dbPath}"); }); // Configura Database PostgreSQL per statistiche avanzate var usePostgres = builder.Configuration.GetValue("Database:UsePostgres", false); if (usePostgres) { try { var connString = builder.Environment.IsProduction() ? builder.Configuration.GetConnectionString("PostgresStatsProduction") : builder.Configuration.GetConnectionString("PostgresStats"); // Sostituisci variabili ambiente in production if (builder.Environment.IsProduction()) { connString = connString? .Replace("${POSTGRES_USER}", Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "autobidder") .Replace("${POSTGRES_PASSWORD}", Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? ""); } if (!string.IsNullOrEmpty(connString)) { builder.Services.AddDbContext(options => { options.UseNpgsql(connString, npgsqlOptions => { npgsqlOptions.EnableRetryOnFailure(3); npgsqlOptions.CommandTimeout(30); }); }); Console.WriteLine("[Startup] PostgreSQL configured for statistics"); } else { Console.WriteLine("[Startup] PostgreSQL connection string not found - using SQLite only"); } } catch (Exception ex) { Console.WriteLine($"[Startup] PostgreSQL configuration failed: {ex.Message} - using SQLite only"); } } else { Console.WriteLine("[Startup] PostgreSQL disabled in configuration - using SQLite only"); } // Registra servizi applicazione come Singleton per condividere stato var htmlCacheService = new HtmlCacheService( maxConcurrentRequests: 3, requestsPerSecond: 5, cacheExpiration: TimeSpan.FromMinutes(5), maxRetries: 2 ); var auctionMonitor = new AuctionMonitor(); htmlCacheService.OnLog += (msg) => Console.WriteLine(msg); builder.Services.AddSingleton(auctionMonitor); builder.Services.AddSingleton(htmlCacheService); builder.Services.AddSingleton(sp => new SessionService(auctionMonitor.GetApiClient())); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(sp => { var db = sp.GetRequiredService(); // Prova a ottenere PostgreSQL context (potrebbe essere null) AutoBidder.Data.PostgresStatsContext? postgresDb = null; try { postgresDb = sp.GetService(); } catch { // PostgreSQL non disponibile, usa solo SQLite } return new StatsService(db, postgresDb); }); builder.Services.AddScoped(); // Configura SignalR per real-time updates builder.Services.AddSignalR(options => { options.MaximumReceiveMessageSize = 102400; // 100KB options.EnableDetailedErrors = true; }); var app = builder.Build(); // ============================================ // INIZIALIZZAZIONE DATABASE IDENTITY // ============================================ using (var scope = app.Services.CreateScope()) { try { var identityDb = scope.ServiceProvider.GetRequiredService(); var userManager = scope.ServiceProvider.GetRequiredService>(); // Crea database Identity await identityDb.Database.EnsureCreatedAsync(); Console.WriteLine("[Identity] Database initialized"); // Crea utente admin se non esiste var adminUsername = Environment.GetEnvironmentVariable("ADMIN_USERNAME") ?? "admin"; var adminPassword = Environment.GetEnvironmentVariable("ADMIN_PASSWORD"); // Password di default se non configurata (stessa per debug e container) if (string.IsNullOrEmpty(adminPassword)) { adminPassword = "Admin@Password123!"; } var existingAdmin = await userManager.FindByNameAsync(adminUsername); if (existingAdmin == null) { var adminUser = new ApplicationUser { UserName = adminUsername, Email = $"{adminUsername}@autobidder.local", EmailConfirmed = true, IsActive = true, CreatedAt = DateTime.UtcNow }; var result = await userManager.CreateAsync(adminUser, adminPassword); if (result.Succeeded) { Console.WriteLine($"[Identity] Admin user created: {adminUsername}"); } else { Console.WriteLine($"[Identity] Failed to create admin user: {string.Join(", ", result.Errors.Select(e => e.Description))}"); } } else { Console.WriteLine($"[Identity] Admin user exists: {adminUsername}"); } } catch (Exception ex) { Console.WriteLine($"[Identity] Initialization error: {ex.Message}"); } } // ??? NUOVO: Inizializza DatabaseService using (var scope = app.Services.CreateScope()) { var databaseService = scope.ServiceProvider.GetRequiredService(); try { Console.WriteLine("[DB] Initializing main database..."); await databaseService.InitializeDatabaseAsync(); var dbInfo = await databaseService.GetDatabaseInfoAsync(); Console.WriteLine($"[DB] Database initialized successfully:"); Console.WriteLine($"[DB] Path: {dbInfo.Path}"); Console.WriteLine($"[DB] Size: {dbInfo.SizeFormatted}"); Console.WriteLine($"[DB] Version: {dbInfo.Version}"); Console.WriteLine($"[DB] Auctions: {dbInfo.AuctionsCount}"); Console.WriteLine($"[DB] Bid History: {dbInfo.BidHistoryCount}"); Console.WriteLine($"[DB] Product Stats: {dbInfo.ProductStatsCount}"); // Verifica salute database var isHealthy = await databaseService.CheckDatabaseHealthAsync(); Console.WriteLine($"[DB] Database health check: {(isHealthy ? "OK" : "FAILED")}"); } catch (Exception ex) { Console.WriteLine($"[DB ERROR] Failed to initialize database: {ex.Message}"); Console.WriteLine($"[DB ERROR] Stack trace: {ex.StackTrace}"); } } // Crea database statistiche se non esiste (senza migrations) using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); try { // Log percorso database var connection = db.Database.GetDbConnection(); Console.WriteLine($"[STATS DB] Database path: {connection.DataSource}"); // Verifica se database esiste var dbExists = db.Database.CanConnect(); Console.WriteLine($"[STATS DB] Database exists: {dbExists}"); // Forza creazione tabelle se non esistono if (!dbExists || !db.ProductStats.Any()) { Console.WriteLine("[STATS DB] Creating database schema..."); db.Database.EnsureDeleted(); // Elimina database vecchio db.Database.EnsureCreated(); // Ricrea con schema aggiornato Console.WriteLine("[STATS DB] Database schema created successfully"); } else { Console.WriteLine($"[STATS DB] Database already exists with {db.ProductStats.Count()} records"); } } catch (Exception ex) { Console.WriteLine($"[STATS DB ERROR] Failed to initialize database: {ex.Message}"); Console.WriteLine($"[STATS DB ERROR] Stack trace: {ex.StackTrace}"); // Prova a ricreare forzatamente try { Console.WriteLine("[STATS DB] Attempting forced recreation..."); db.Database.EnsureDeleted(); db.Database.EnsureCreated(); Console.WriteLine("[STATS DB] Forced recreation successful"); } catch (Exception ex2) { Console.WriteLine($"[STATS DB ERROR] Forced recreation failed: {ex2.Message}"); } } } // Inizializza PostgreSQL per statistiche avanzate using (var scope = app.Services.CreateScope()) { try { var postgresDb = scope.ServiceProvider.GetService(); if (postgresDb != null) { Console.WriteLine("[PostgreSQL] Initializing PostgreSQL statistics database..."); var autoCreateSchema = app.Configuration.GetValue("Database:AutoCreateSchema", true); if (autoCreateSchema) { // Usa il metodo EnsureSchemaAsync che gestisce la creazione automatica var schemaCreated = await postgresDb.EnsureSchemaAsync(); if (schemaCreated) { // Valida che tutte le tabelle siano state create var schemaValid = await postgresDb.ValidateSchemaAsync(); if (schemaValid) { Console.WriteLine("[PostgreSQL] Statistics features ENABLED"); } else { Console.WriteLine("[PostgreSQL] Schema validation failed"); Console.WriteLine("[PostgreSQL] Statistics features DISABLED (missing tables)"); } } else { Console.WriteLine("[PostgreSQL] Cannot connect to database"); Console.WriteLine("[PostgreSQL] Statistics features will use SQLite fallback"); } } else { Console.WriteLine("[PostgreSQL] Auto-create schema disabled"); // Prova comunque a validare lo schema esistente try { var schemaValid = await postgresDb.ValidateSchemaAsync(); if (schemaValid) { Console.WriteLine("[PostgreSQL] Existing schema validated successfully"); Console.WriteLine("[PostgreSQL] Statistics features ENABLED"); } else { Console.WriteLine("[PostgreSQL] Statistics features DISABLED (schema not found)"); } } catch { Console.WriteLine("[PostgreSQL] Statistics features DISABLED (schema not found)"); } } } else { Console.WriteLine("[PostgreSQL] Not configured - Statistics will use SQLite only"); } } catch (Exception ex) { Console.WriteLine($"[PostgreSQL ERROR] Initialization failed: {ex.Message}"); Console.WriteLine($"[PostgreSQL ERROR] Stack trace: {ex.StackTrace}"); Console.WriteLine($"[PostgreSQL] Statistics features will use SQLite fallback"); } } // ??? NUOVO: Ripristina aste salvate e riprendi monitoraggio se configurato using (var scope = app.Services.CreateScope()) { try { Console.WriteLine("[STARTUP] Loading saved auctions..."); // Carica impostazioni var settings = AutoBidder.Utilities.SettingsManager.Load(); Console.WriteLine($"[STARTUP] Remember auction states: {settings.RememberAuctionStates}"); Console.WriteLine($"[STARTUP] Default start on load: {settings.DefaultStartAuctionsOnLoad}"); // Carica aste salvate var savedAuctions = AutoBidder.Utilities.PersistenceManager.LoadAuctions(); Console.WriteLine($"[STARTUP] Found {savedAuctions.Count} saved auctions"); if (savedAuctions.Count > 0) { var monitor = scope.ServiceProvider.GetRequiredService(); var appState = scope.ServiceProvider.GetRequiredService(); // Aggiungi tutte le aste al monitor E a ApplicationStateService foreach (var auction in savedAuctions) { monitor.AddAuction(auction); Console.WriteLine($"[STARTUP] Loaded auction: {auction.Name} (ID: {auction.AuctionId})"); } // Popola ApplicationStateService con le aste caricate appState.SetAuctions(savedAuctions); Console.WriteLine($"[STARTUP] Populated ApplicationStateService with {savedAuctions.Count} auctions"); // Gestisci comportamento di avvio if (settings.RememberAuctionStates) { // Modalità "Ricorda Stato": mantiene lo stato salvato di ogni asta var activeAuctions = savedAuctions.Where(a => a.IsActive && !a.IsPaused).ToList(); if (activeAuctions.Any()) { Console.WriteLine($"[STARTUP] Resuming monitoring for {activeAuctions.Count} active auctions"); monitor.Start(); appState.IsMonitoringActive = true; appState.AddLog($"[STARTUP] Ripristinato stato salvato: {activeAuctions.Count} aste attive"); } else { Console.WriteLine("[STARTUP] No active auctions to resume"); appState.AddLog("[STARTUP] Nessuna asta attiva salvata"); } } else { // Modalità "Default": applica DefaultStartAuctionsOnLoad a tutte le aste switch (settings.DefaultStartAuctionsOnLoad) { case "Active": // Avvia tutte le aste Console.WriteLine("[STARTUP] Starting all auctions (Active mode)"); foreach (var auction in savedAuctions) { auction.IsActive = true; auction.IsPaused = false; } monitor.Start(); appState.IsMonitoringActive = true; appState.AddLog($"[AUTO-START] Avviate automaticamente {savedAuctions.Count} aste"); break; case "Paused": // Mette in pausa tutte le aste Console.WriteLine("[STARTUP] Starting in paused mode"); foreach (var auction in savedAuctions) { auction.IsActive = true; auction.IsPaused = true; } monitor.Start(); appState.IsMonitoringActive = true; appState.AddLog($"[AUTO-START] Aste in pausa: {savedAuctions.Count}"); break; case "Stopped": default: // Ferma tutte le aste (default) Console.WriteLine("[STARTUP] Starting in stopped mode"); foreach (var auction in savedAuctions) { auction.IsActive = false; auction.IsPaused = false; } appState.AddLog($"[STARTUP] Aste fermate all'avvio: {savedAuctions.Count}"); break; } } } else { Console.WriteLine("[STARTUP] No saved auctions found"); } } catch (Exception ex) { Console.WriteLine($"[STARTUP ERROR] Failed to load auctions: {ex.Message}"); Console.WriteLine($"[STARTUP ERROR] Stack trace: {ex.StackTrace}"); } } // Configure the HTTP request pipeline if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // Abilita HSTS solo se HTTPS è attivo if (enableHttps) { app.UseHsts(); } } else { app.UseDeveloperExceptionPage(); } // Abilita HTTPS redirection solo se HTTPS è configurato if (enableHttps) { app.UseHttpsRedirection(); } app.UseStaticFiles(); app.UseRouting(); // ============================================ // MIDDLEWARE AUTENTICAZIONE E AUTORIZZAZIONE // ============================================ app.UseAuthentication(); app.UseAuthorization(); app.MapRazorPages(); // ? AGGIUNTO: abilita Razor Pages (Login, Logout) app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); app.Run();