using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Purrse.Api.Hubs; using Purrse.Api.Middleware; using Purrse.Api.Services; using Purrse.Core.Interfaces.Services; using Purrse.Data; using Purrse.Plugins.Abstractions; using Purrse.Plugins.BankSync; using Purrse.Plugins.CSV; using Purrse.Plugins.OFX; using Purrse.Plugins.QIF; var builder = WebApplication.CreateBuilder(args); // Database builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); // JWT Authentication var jwtKey = builder.Configuration["Jwt:Key"] ?? "PurrseDefaultSecretKey_ChangeInProduction_AtLeast32Chars!"; builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"] ?? "Purrse", ValidAudience = builder.Configuration["Jwt:Audience"] ?? "Purrse", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)) }; // SignalR JWT support 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; } }; }); // CORS builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.WithOrigins( builder.Configuration.GetSection("Cors:Origins").Get() ?? new[] { "http://localhost:5173", "http://localhost:8080" }) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); // Services 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.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient("Ollama"); // Built-in file parsers builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Bank sync providers builder.Services.AddSingleton(); // Stateless — credentials passed per-call builder.Services.AddHttpClient(); // Plugin system builder.Services.AddSingleton(); // Background services builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); // SignalR builder.Services.AddSignalR(); builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); }); builder.Services.AddEndpointsApiExplorer(); var app = builder.Build(); // Auto-migrate database using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); await db.Database.MigrateAsync(); } // One-time data migrations await SimpleFinConnectionMigration.RunAsync(app.Services, app.Services.GetRequiredService>()); using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); await db.Database.ExecuteSqlRawAsync( "CREATE TABLE IF NOT EXISTS data_migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMP NOT NULL)"); var applied = await db.Database.SqlQueryRaw( "SELECT 1 AS \"Value\" FROM data_migrations WHERE name = 'ClearSyncedTransactions'").AnyAsync(); if (!applied) { var count = await db.Database.ExecuteSqlRawAsync( "UPDATE \"Transactions\" SET \"Status\" = 1 WHERE \"FitId\" IS NOT NULL AND \"Status\" = 0"); if (count > 0) app.Services.GetRequiredService>().LogInformation("Marked {Count} bank-synced transactions as Cleared", count); await db.Database.ExecuteSqlRawAsync( "INSERT INTO data_migrations (name, applied_at) VALUES ('ClearSyncedTransactions', NOW()) ON CONFLICT DO NOTHING"); } } // Initialize plugin system var pluginService = app.Services.GetRequiredService(); await pluginService.DiscoverAndLoadPluginsAsync(); // Middleware app.UseMiddleware(); app.UseMiddleware(); app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapHub("/hubs/notifications"); app.Run();