littleshop/LittleShop/Program.cs
SysAdmin e61b055512 Fix CORS policy for admin.dark.side domain
- Add https://admin.dark.side and http://admin.dark.side to ProductionCors allowed origins
- Increment version to 1.0.6
- Fixes push notification CORS blocking issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-01 03:22:40 +01:00

446 lines
16 KiB
C#

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;
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<LittleShopContext>(options =>
options.UseInMemoryDatabase("InMemoryDbForTesting"));
}
else
{
builder.Services.AddDbContext<LittleShopContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
}
// Rate Limiting - protect anonymous endpoints
builder.Services.AddMemoryCache();
builder.Services.Configure<AspNetCoreRateLimit.IpRateLimitOptions>(options =>
{
options.EnableEndpointRateLimiting = true;
options.StackBlockedRequests = false;
options.HttpStatusCode = 429;
options.RealIpHeader = "X-Real-IP";
options.ClientIdHeader = "X-ClientId";
options.GeneralRules = new List<AspNetCoreRateLimit.RateLimitRule>
{
// 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<AspNetCoreRateLimit.IIpPolicyStore, AspNetCoreRateLimit.MemoryCacheIpPolicyStore>();
builder.Services.AddSingleton<AspNetCoreRateLimit.IRateLimitCounterStore, AspNetCoreRateLimit.MemoryCacheRateLimitCounterStore>();
builder.Services.AddSingleton<AspNetCoreRateLimit.IRateLimitConfiguration, AspNetCoreRateLimit.RateLimitConfiguration>();
builder.Services.AddSingleton<AspNetCoreRateLimit.IProcessingStrategy, AspNetCoreRateLimit.AsyncKeyLockProcessingStrategy>();
// Authentication - Cookie for Admin Panel, JWT for API
var jwtKey = builder.Configuration["Jwt:Key"];
if (string.IsNullOrEmpty(jwtKey))
{
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\" \"<your-secure-key>\"");
}
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "LittleShop";
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "LittleShop";
builder.Services.AddAuthentication("Cookies")
.AddCookie("Cookies", options =>
{
options.LoginPath = "/Admin/Account/Login";
options.LogoutPath = "/Admin/Account/Logout";
options.AccessDeniedPath = "/Admin/Account/AccessDenied";
})
.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", "Bearer")); // Support both cookie and JWT
options.AddPolicy("ApiAccess", policy =>
policy.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Bearer")); // JWT only for API access
});
// Services
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<ICategoryService, CategoryService>();
builder.Services.AddScoped<IVariantCollectionService, VariantCollectionService>();
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<ICryptoPaymentService, CryptoPaymentService>();
// 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<ISilverPayService, SilverPayService>();
builder.Services.AddScoped<IShippingRateService, ShippingRateService>();
builder.Services.AddScoped<IRoyalMailService, RoyalMailShippingService>();
builder.Services.AddHttpClient<IRoyalMailService, RoyalMailShippingService>();
builder.Services.AddScoped<IReviewService, ReviewService>();
builder.Services.AddScoped<IDataSeederService, DataSeederService>();
builder.Services.AddScoped<IBotService, BotService>();
builder.Services.AddScoped<IBotMetricsService, BotMetricsService>();
builder.Services.AddScoped<IBotContactService, BotContactService>();
builder.Services.AddScoped<IMessageDeliveryService, MessageDeliveryService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.AddScoped<ICustomerMessageService, CustomerMessageService>();
builder.Services.AddScoped<IPushNotificationService, PushNotificationService>();
builder.Services.AddHttpClient<ITeleBotMessagingService, TeleBotMessagingService>();
builder.Services.AddScoped<IProductImportService, ProductImportService>();
builder.Services.AddSingleton<ITelegramBotManagerService, TelegramBotManagerService>();
builder.Services.AddScoped<IBotActivityService, BotActivityService>();
builder.Services.AddScoped<ISystemSettingsService, SystemSettingsService>();
// Configuration validation service
builder.Services.AddSingleton<ConfigurationValidationService>();
// SignalR
builder.Services.AddSignalR();
// Health Checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<LittleShopContext>("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<TelegramBotManagerService>();
// AutoMapper
builder.Services.AddAutoMapper(typeof(Program));
// FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
// 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<string>()
}
});
});
// 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<string[]>()
?? 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<string[]>()
?? 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<ConfigurationValidationService>();
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 hub
app.MapHub<LittleShop.Hubs.ActivityHub>("/activityHub");
// 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<LittleShopContext>();
// 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<IAuthService>();
await authService.SeedDefaultUserAsync();
// Seed sample data
var dataSeeder = scope.ServiceProvider.GetRequiredService<IDataSeederService>();
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<ISystemSettingsService>();
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 { }