using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; using LittleShop.Data; using LittleShop.Services; using FluentValidation; using Serilog; using AspNetCoreRateLimit; using LittleShop.Configuration; var builder = WebApplication.CreateBuilder(args); // Configure Serilog Log.Logger = new LoggerConfiguration() .WriteTo.Console() .WriteTo.File("logs/littleshop.txt", rollingInterval: RollingInterval.Day) .CreateLogger(); builder.Host.UseSerilog(); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddControllersWithViews(); // Add MVC for Admin Panel builder.Services.AddRazorPages(); // Add Razor Pages for Blazor builder.Services.AddServerSideBlazor(); // Add Blazor Server // Configure Antiforgery builder.Services.AddAntiforgery(options => { options.HeaderName = "X-CSRF-TOKEN"; options.FormFieldName = "__RequestVerificationToken"; }); // Database if (builder.Environment.EnvironmentName == "Testing") { builder.Services.AddDbContext(options => options.UseInMemoryDatabase("InMemoryDbForTesting")); } else { builder.Services.AddDbContext(options => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")) .ConfigureWarnings(warnings => warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning))); } // Rate Limiting - protect anonymous endpoints builder.Services.AddMemoryCache(); builder.Services.Configure(options => { options.EnableEndpointRateLimiting = true; options.StackBlockedRequests = false; options.HttpStatusCode = 429; options.RealIpHeader = "X-Real-IP"; options.ClientIdHeader = "X-ClientId"; options.GeneralRules = new List { // Critical: Order creation - very strict limits new AspNetCoreRateLimit.RateLimitRule { Endpoint = "POST:*/api/orders", Period = "1m", Limit = 3 }, new AspNetCoreRateLimit.RateLimitRule { Endpoint = "POST:*/api/orders", Period = "1h", Limit = 10 }, // Critical: Payment creation - strict limits new AspNetCoreRateLimit.RateLimitRule { Endpoint = "POST:*/api/orders/*/payments", Period = "1m", Limit = 5 }, new AspNetCoreRateLimit.RateLimitRule { Endpoint = "POST:*/api/orders/*/payments", Period = "1h", Limit = 20 }, // Order lookup by identity - moderate limits new AspNetCoreRateLimit.RateLimitRule { Endpoint = "*/api/orders/by-identity/*", Period = "1m", Limit = 10 }, new AspNetCoreRateLimit.RateLimitRule { Endpoint = "*/api/orders/by-customer/*", Period = "1m", Limit = 10 }, // Cancel order endpoint - moderate limits new AspNetCoreRateLimit.RateLimitRule { Endpoint = "POST:*/api/orders/*/cancel", Period = "1m", Limit = 5 }, // Webhook endpoint - exempt from rate limiting new AspNetCoreRateLimit.RateLimitRule { Endpoint = "POST:*/api/orders/payments/webhook", Period = "1s", Limit = 1000 }, // General API limits new AspNetCoreRateLimit.RateLimitRule { Endpoint = "*", Period = "1s", Limit = 10 }, new AspNetCoreRateLimit.RateLimitRule { Endpoint = "*", Period = "1m", Limit = 100 } }; }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Authentication - Cookie for Admin Panel, JWT for API var jwtKey = builder.Configuration["Jwt:Key"]; if (string.IsNullOrEmpty(jwtKey) && builder.Environment.EnvironmentName != "Testing") { Log.Fatal("🚨 SECURITY: Jwt:Key configuration is missing. Application cannot start securely."); throw new InvalidOperationException( "JWT:Key must be configured in environment variables or user secrets. " + "Set the Jwt__Key environment variable or use: dotnet user-secrets set \"Jwt:Key\" \"\""); } // Use test key for testing environment if (builder.Environment.EnvironmentName == "Testing" && string.IsNullOrEmpty(jwtKey)) { jwtKey = "test-key-that-is-at-least-32-characters-long-for-security"; } var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "LittleShop"; var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "LittleShop"; builder.Services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "Cookies"; }) .AddCookie("Cookies", options => { options.LoginPath = "/Admin/Account/Login"; options.LogoutPath = "/Admin/Account/Logout"; options.AccessDeniedPath = "/Admin/Account/AccessDenied"; options.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents { OnRedirectToLogin = context => { // For admin routes, always redirect to login page if (context.Request.Path.StartsWithSegments("/Admin")) { context.Response.StatusCode = 302; context.Response.Headers["Location"] = context.RedirectUri; return Task.CompletedTask; } // For API routes, return 401 context.Response.StatusCode = 401; return Task.CompletedTask; } }; }) .AddJwtBearer("Bearer", options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtIssuer, ValidAudience = jwtAudience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)) }; }); builder.Services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => policy.RequireAuthenticatedUser() .RequireRole("Admin") .AddAuthenticationSchemes("Cookies")); // Only use cookies for admin panel options.AddPolicy("ApiAccess", policy => policy.RequireAuthenticatedUser() .AddAuthenticationSchemes("Bearer")); // JWT only for API access }); // Services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // BTCPay removed - using SilverPAY only // Production-only SilverPAY service - no mock services allowed in production if (builder.Environment.IsDevelopment()) { // In development, still require real SilverPAY - no fake payments Console.WriteLine("🔒 Development mode: Using real SilverPAY service"); } // Always use real SilverPAY service - mock services removed for security builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Configuration validation service builder.Services.AddSingleton(); // Configure Data Retention Options builder.Services.Configure( builder.Configuration.GetSection(DataRetentionOptions.SectionName)); // Data Retention Background Service builder.Services.AddHostedService(); // SignalR builder.Services.AddSignalR(); // Health Checks builder.Services.AddHealthChecks() .AddDbContextCheck("database") .AddCheck("self", () => Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult.Healthy("Application is healthy")); // Temporarily disabled to use standalone TeleBot with customer orders fix // builder.Services.AddHostedService(); // AutoMapper builder.Services.AddAutoMapper(typeof(Program)); // FluentValidation builder.Services.AddValidatorsFromAssemblyContaining(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "LittleShop API", Version = "v1", Description = "A basic online sales system backend with multi-cryptocurrency payment support", Contact = new Microsoft.OpenApi.Models.OpenApiContact { Name = "LittleShop Support" } }); // Add JWT authentication to Swagger c.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme { Description = "JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below.", Name = "Authorization", In = Microsoft.OpenApi.Models.ParameterLocation.Header, Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey, Scheme = "Bearer" }); c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement { { new Microsoft.OpenApi.Models.OpenApiSecurityScheme { Reference = new Microsoft.OpenApi.Models.OpenApiReference { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty() } }); }); // CORS - Configure for both development and production builder.Services.AddCors(options => { // Development CORS policy - configured from appsettings options.AddPolicy("DevelopmentCors", corsBuilder => { var allowedOrigins = builder.Configuration.GetSection("CORS:AllowedOrigins").Get() ?? new[] { "http://localhost:3000", "http://localhost:5173", "http://localhost:5000" }; corsBuilder.WithOrigins(allowedOrigins) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); // Important for cookie authentication }); // Production CORS policy - strict security options.AddPolicy("ProductionCors", corsBuilder => { var allowedOrigins = builder.Configuration.GetSection("CORS:AllowedOrigins").Get() ?? new[] { "https://littleshop.silverlabs.uk", "https://admin.dark.side", "http://admin.dark.side" }; corsBuilder.WithOrigins(allowedOrigins) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); // API-specific CORS policy (no credentials for public API) options.AddPolicy("ApiCors", corsBuilder => { // Public API should have more restricted CORS corsBuilder.WithOrigins("https://littleshop.silverlabs.uk", "https://pay.silverlabs.uk") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); }); var app = builder.Build(); // Validate configuration on startup - fail fast if misconfigured try { var configValidator = app.Services.GetRequiredService(); configValidator.ValidateConfiguration(); } catch (Exception ex) { Log.Fatal(ex, "🚨 STARTUP FAILED: Configuration validation error"); throw; } // Configure the HTTP request pipeline. // Add CORS early in the pipeline - before authentication if (app.Environment.IsDevelopment()) { app.UseCors("DevelopmentCors"); app.UseSwagger(); app.UseSwaggerUI(); } else { // Use production CORS policy in production environment app.UseCors("ProductionCors"); } // Add error handling middleware for production if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); // Use HSTS for production security } // Add rate limiting middleware (after CORS, before authentication) app.UseIpRateLimiting(); app.UseStaticFiles(); // Enable serving static files app.UseAuthentication(); app.UseAuthorization(); // Configure routing app.MapControllerRoute( name: "admin", pattern: "Admin/{controller=Dashboard}/{action=Index}/{id?}", defaults: new { area = "Admin" } ); app.MapControllerRoute( name: "areas", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.MapControllers(); // API routes app.MapBlazorHub(); // Map Blazor Server hub app.MapRazorPages(); // Enable Razor Pages for Blazor app.MapFallbackToPage("/blazor/{*path}", "/_Host"); // Fallback for all Blazor routes // Map SignalR hubs app.MapHub("/activityHub"); app.MapHub("/notificationHub"); // Health check endpoint app.MapHealthChecks("/health"); // Version endpoint app.MapGet("/api/version", () => { var version = typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0"; var assemblyVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0"; return Results.Ok(new { version = assemblyVersion, environment = builder.Environment.EnvironmentName, application = "LittleShop" }); }); // Apply database migrations and seed data using (var scope = app.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); // Use proper migrations in production, EnsureCreated only for development/testing if (app.Environment.IsProduction()) { Log.Information("Production environment: Applying database migrations..."); try { context.Database.Migrate(); Log.Information("Database migrations applied successfully"); } catch (Exception ex) { Log.Fatal(ex, "Database migration failed. Application cannot start."); throw; } } else { Log.Information("Development/Testing environment: Using EnsureCreated"); context.Database.EnsureCreated(); } // Seed default admin user var authService = scope.ServiceProvider.GetRequiredService(); await authService.SeedDefaultUserAsync(); // Seed sample data var dataSeeder = scope.ServiceProvider.GetRequiredService(); await dataSeeder.SeedSampleDataAsync(); // Seed system settings - enable test currencies only in development if (app.Environment.IsDevelopment()) { Log.Information("Development environment: Enabling test currencies"); var systemSettings = scope.ServiceProvider.GetRequiredService(); await systemSettings.SetTestCurrencyEnabledAsync("TBTC", true); await systemSettings.SetTestCurrencyEnabledAsync("TLTC", true); } } Log.Information("LittleShop API starting up..."); app.Run(); // Make Program accessible to test project public partial class Program { }