Files
Mimante/Mimante/Program.cs
Alberto Balbo 865bfa2752 Aggiunta pagina "Esplora Aste" per browser pubblico
Introdotta la funzionalità di esplorazione delle aste pubbliche di Bidoo senza login, accessibile dal menu principale.
Aggiunti nuovi modelli (`BidooBrowserAuction`, `BidooCategoryInfo`) e servizio (`BidooBrowserService`) per scraping e polling delle aste e categorie.
Creata la pagina Blazor `AuctionBrowser.razor` con griglia responsive, badge, filtri per categoria, caricamento incrementale e aggiornamento automatico degli stati.
Aggiornati i servizi in `Program.cs` e aggiunti nuovi stili CSS per la UI moderna.
Le aste possono essere aggiunte rapidamente al monitor personale. Parsing robusto e fallback su categorie predefinite in caso di errori.
2026-01-22 00:08:16 +01:00

631 lines
23 KiB
C#

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<bool>("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<ApplicationDbContext>(options =>
{
options.UseSqlite($"Data Source={identityDbPath}");
});
// ASP.NET Core Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(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<ApplicationDbContext>()
.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<StatisticsContext>(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<bool>("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<AutoBidder.Data.PostgresStatsContext>(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<DatabaseService>();
builder.Services.AddSingleton<ApplicationStateService>();
builder.Services.AddSingleton<BidooBrowserService>();
builder.Services.AddScoped<StatsService>(sp =>
{
var db = sp.GetRequiredService<DatabaseService>();
// Prova a ottenere PostgreSQL context (potrebbe essere null)
AutoBidder.Data.PostgresStatsContext? postgresDb = null;
try
{
postgresDb = sp.GetService<AutoBidder.Data.PostgresStatsContext>();
}
catch
{
// PostgreSQL non disponibile, usa solo SQLite
}
return new StatsService(db, postgresDb);
});
builder.Services.AddScoped<AuctionStateService>();
// 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<ApplicationDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
// 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<DatabaseService>();
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<StatisticsContext>();
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<AutoBidder.Data.PostgresStatsContext>();
if (postgresDb != null)
{
Console.WriteLine("[PostgreSQL] Initializing PostgreSQL statistics database...");
var autoCreateSchema = app.Configuration.GetValue<bool>("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<AuctionMonitor>();
var appState = scope.ServiceProvider.GetRequiredService<ApplicationStateService>();
// 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();