feat(Backend): update API gateway and websocket hubs
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
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<BackendDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
builder.Services.AddHttpClient<UserTradesController>();
|
||||
builder.Services.AddHttpClient<UserFavoritesController>();
|
||||
builder.Services.AddHttpClient<NewsController>();
|
||||
builder.Services.AddHttpClient<AssetsController>();
|
||||
builder.Services.AddHttpClient<IFirebaseNotificationService, FirebaseNotificationService>();
|
||||
|
||||
builder.Services.AddSingleton<IJwtTokenService, JwtTokenService>();
|
||||
builder.Services.AddSingleton<IUserService, UserService>();
|
||||
builder.Services.AddSingleton<IFirebaseNotificationService, FirebaseNotificationService>();
|
||||
|
||||
builder.Services.AddSingleton<WebMqttClient>();
|
||||
builder.Services.AddHostedService<WebMqttClient>(sp => sp.GetRequiredService<WebMqttClient>());
|
||||
builder.Services.AddHostedService<BackendMqttBridge>();
|
||||
builder.Services.AddHostedService<SystemHealthBackgroundService>();
|
||||
builder.Services.AddHostedService<FavoritesPriceBackgroundService>();
|
||||
|
||||
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<BackendDbContext>();
|
||||
bool userExists = await dbContext.Users.AsNoTracking().AnyAsync(u => u.Id == userId && u.IsActive);
|
||||
if (!userExists)
|
||||
{
|
||||
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
|
||||
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<BackendDbContext>();
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
|
||||
string adminDefaultPassword = builder.Configuration["ADMIN:DefaultPassword"] ?? "AdminDefaultPassword2026!";
|
||||
await userService.SeedDefaultAdminAsync(adminDefaultPassword);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
|
||||
logger.LogError(ex, "An error occurred during database migration/seeding.");
|
||||
}
|
||||
}
|
||||
|
||||
// Map Endpoints
|
||||
app.MapControllers();
|
||||
|
||||
app.MapHub<TradeRealtimeHub>("/hubs/trades", options =>
|
||||
{
|
||||
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents;
|
||||
});
|
||||
app.MapHub<TradeHub>("/hubs/trade-updates", options =>
|
||||
{
|
||||
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents;
|
||||
});
|
||||
app.MapHub<NewsHub>("/hubs/news", options =>
|
||||
{
|
||||
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents;
|
||||
});
|
||||
app.MapHub<SystemHealthHub>("/hubs/health", options =>
|
||||
{
|
||||
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents;
|
||||
});
|
||||
app.MapHub<FavoritesPriceHub>("/hubs/favorites-prices", 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();
|
||||
Reference in New Issue
Block a user