using System.Security.Claims; using System.Text; using System.Threading.Tasks; using FinlyticBackend.Controllers; using FinlyticBackend.Database; using FinlyticBackend.Hubs; using FinlyticBackend.Services; using FinlyticBackend.Util; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Connections; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); // 1. Add Controllers builder.Services.AddControllers(); // 2. Define CORS Policy builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => { policy.SetIsOriginAllowed(_ => true) // Allows any origin including Flutter Web localhost .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); // 3. Configure JWT Authentication var secretKey = builder.Configuration["JWT:SecretKey"] ?? "FinlyticEnterpriseUltraSecureJwtSecretKey_2026_AtLeast32Chars!"; var issuer = builder.Configuration["JWT:Issuer"] ?? "FinlyticBackend"; var audience = builder.Configuration["JWT:Audience"] ?? "FinlyticClients"; builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.SaveToken = true; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)), ValidateIssuer = true, ValidIssuer = issuer, ValidateAudience = true, ValidAudience = audience, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(5) }; options.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) { context.Token = accessToken; } return Task.CompletedTask; } }; }); builder.Services.AddAuthorization(); builder.Services.AddSignalR(); // 4. Register DB Context & Services builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); var app = builder.Build(); // ---------------------------------------------------------------------- // MIDDLEWARE PIPELINE ORDER IS CRITICAL FOR CORS // ---------------------------------------------------------------------- // 1. Static & Default files app.UseDefaultFiles(); app.UseStaticFiles(); // 2. Routing MUST come before UseCors app.UseRouting(); // 3. CORS MUST come after UseRouting and before UseAuthentication/UseAuthorization app.UseCors("AllowAll"); // 4. Authentication & Authorization app.UseAuthentication(); app.UseAuthorization(); // 5. Global User DB Validation Middleware (ensures JWT userId exists in PostgreSQL users table for every authorized route) app.Use(async (context, next) => { if (context.User.Identity?.IsAuthenticated == true) { var userIdStr = context.User.FindFirstValue(ClaimTypes.NameIdentifier); if (Guid.TryParse(userIdStr, out var userId)) { var dbContext = context.RequestServices.GetRequiredService(); bool userExists = await dbContext.Users.AsNoTracking().AnyAsync(u => u.Id == userId && u.IsActive); if (!userExists) { var logger = context.RequestServices.GetRequiredService>(); logger.LogWarning("[UserValidation] Authenticated request for UserId '{UserId}' failed DB validation (User not found or inactive). Returning 401 Unauthorized.", userId); context.Response.StatusCode = Microsoft.AspNetCore.Http.StatusCodes.Status401Unauthorized; context.Response.ContentType = "application/json"; await context.Response.WriteAsync("{\"message\":\"User account does not exist or has been deactivated.\"}"); return; } } else { context.Response.StatusCode = Microsoft.AspNetCore.Http.StatusCodes.Status401Unauthorized; context.Response.ContentType = "application/json"; await context.Response.WriteAsync("{\"message\":\"Invalid user claim in authentication token.\"}"); return; } } await next(); }); // Database Migration & Seeding using (var scope = app.Services.CreateScope()) { try { var context = scope.ServiceProvider.GetRequiredService(); await context.Database.MigrateAsync(); var userService = scope.ServiceProvider.GetRequiredService(); string adminDefaultPassword = builder.Configuration["ADMIN:DefaultPassword"] ?? "AdminDefaultPassword2026!"; await userService.SeedDefaultAdminAsync(adminDefaultPassword); } catch (Exception ex) { var logger = scope.ServiceProvider.GetRequiredService>(); logger.LogError(ex, "An error occurred during database migration/seeding."); } } // Map Endpoints app.MapControllers(); app.MapHub("/hubs/trades", options => { options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents; }); app.MapHub("/hubs/trade-updates", options => { options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents; }); app.MapHub("/hubs/news", options => { options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents; }); app.MapHub("/hubs/health", options => { options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents; }); app.MapHub("/hubs/favorites-prices", options => { options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents; }); app.MapHub("/hubs/logs", options => { options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents; }); app.MapGet("/health", () => Results.Ok(new { status = "Healthy", service = "FinlyticBackend", timestamp = DateTime.UtcNow })); app.MapFallbackToFile("index.html"); await app.RunAsync();