using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Reflection; using System.Text; using DiunaBI.API.Hubs; using DiunaBI.API.Services; using DiunaBI.Infrastructure.Data; using DiunaBI.Infrastructure.Interceptors; using DiunaBI.Infrastructure.Services; using Google.Apis.Sheets.v4; using Serilog; var builder = WebApplication.CreateBuilder(args); if (builder.Environment.IsProduction()) { builder.Host.UseSerilog((context, configuration) => { var instanceName = context.Configuration["InstanceName"] ?? "unknown"; configuration .ReadFrom.Configuration(context.Configuration) .Enrich.FromLogContext() .Enrich.WithProperty("Application", $"DiunaBI-{instanceName}") .Enrich.WithProperty("Version", Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown") .Enrich.WithEnvironmentName() .Enrich.WithMachineName(); }); } var connectionString = builder.Configuration.GetConnectionString("SQLDatabase"); // Register EntityChangeInterceptor builder.Services.AddSingleton(); builder.Services.AddDbContext((serviceProvider, options) => { options.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly("DiunaBI.Infrastructure")); options.EnableSensitiveDataLogging(); // Add EntityChangeInterceptor var interceptor = serviceProvider.GetRequiredService(); options.AddInterceptors(interceptor); }); builder.Services.AddCors(options => { options.AddPolicy("CORSPolicy", corsPolicyBuilder => { corsPolicyBuilder.WithOrigins("http://localhost:4200") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); corsPolicyBuilder.WithOrigins("https://diuna.bim-it.pl") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); corsPolicyBuilder.WithOrigins("https://morska.diunabi.com") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); }); builder.Services.AddControllers(); // SignalR builder.Services.AddSignalR(); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["JwtSettings:Issuer"], ValidAudience = builder.Configuration["JwtSettings:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:SecurityKey"]!)) }; }); builder.Services.AddScoped(); builder.Services.AddScoped(); // Google Sheets dependencies Console.WriteLine("Adding Google Sheets dependencies..."); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(provider => { var googleSheetsHelper = provider.GetRequiredService(); var valuesResource = googleSheetsHelper.Service?.Spreadsheets.Values; if (valuesResource == null) { throw new InvalidOperationException("Google Sheets Service is not initialized properly"); } return valuesResource; }); builder.Services.AddSingleton(); // Job Queue Services builder.Services.AddScoped(); builder.Services.AddHostedService(); var app = builder.Build(); // Auto-apply migrations on startup using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService>(); db.Database.SetCommandTimeout(TimeSpan.FromMinutes(5)); try { await db.Database.OpenConnectionAsync(); await db.Database.ExecuteSqlRawAsync( "EXEC sp_getapplock @Resource = N'DiunaBI_Migrations', @LockMode = 'Exclusive', @LockTimeout = 60000;"); logger.LogInformation("Ensuring database is up to date..."); await db.Database.MigrateAsync(); logger.LogInformation("Database is up to date."); } catch (Exception ex) { logger.LogCritical(ex, "Migration failed - application will not start."); throw; } finally { try { await db.Database.ExecuteSqlRawAsync( "EXEC sp_releaseapplock @Resource = N'DiunaBI_Migrations';"); } catch { /* ignore */ } await db.Database.CloseConnectionAsync(); } } if (app.Environment.IsProduction()) { app.UseSerilogRequestLogging(options => { options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => { diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value); diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme); var userAgent = httpContext.Request.Headers.UserAgent.FirstOrDefault(); if (!string.IsNullOrEmpty(userAgent)) { diagnosticContext.Set("UserAgent", userAgent); } diagnosticContext.Set("RemoteIP", httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"); diagnosticContext.Set("RequestContentType", httpContext.Request.ContentType ?? "none"); }; }); } // Plugin initialization var pluginManager = app.Services.GetRequiredService(); var executablePath = Assembly.GetExecutingAssembly().Location; var executableDir = Path.GetDirectoryName(executablePath)!; var pluginsPath = Path.Combine(executableDir, "Plugins"); if (app.Environment.IsProduction()) { Log.Information("Starting DiunaBI application"); Log.Information("Loading plugins from: {PluginsPath}", pluginsPath); } else { var logger = app.Services.GetRequiredService>(); logger.LogInformation("Starting DiunaBI application (Development)"); logger.LogInformation("Loading plugins from: {PluginsPath}", pluginsPath); } pluginManager.LoadPluginsFromDirectory(pluginsPath); app.UseCors("CORSPolicy"); app.UseAuthentication(); app.UseAuthorization(); // Middleware to extract UserId from JWT token AFTER authentication // This must run after UseAuthentication() so the JWT is already validated app.Use(async (context, next) => { var logger = context.RequestServices.GetRequiredService>(); logger.LogInformation("🔍 UserId Extraction Middleware - Path: {Path}, Method: {Method}", context.Request.Path, context.Request.Method); var token = context.Request.Headers.Authorization.ToString(); logger.LogInformation("🔍 Authorization header: {Token}", string.IsNullOrEmpty(token) ? "NULL/EMPTY" : $"{token[..Math.Min(30, token.Length)]}..."); if (!string.IsNullOrEmpty(token) && token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { try { var handler = new JwtSecurityTokenHandler(); var jwtToken = handler.ReadJwtToken(token.Split(' ')[1]); // Try to get UserId from Subject claim first, then fall back to NameIdentifier var userId = jwtToken.Subject; if (string.IsNullOrEmpty(userId)) { // Try NameIdentifier claim (ClaimTypes.NameIdentifier) var nameIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" || c.Type == "nameid"); userId = nameIdClaim?.Value; } logger.LogInformation("🔍 JWT UserId: {UserId}", userId ?? "NULL"); if (!string.IsNullOrEmpty(userId)) { // Use indexer to set/replace header value instead of Append context.Request.Headers["UserId"] = userId; logger.LogInformation("✅ Set UserId header to: {UserId}", userId); } else { logger.LogWarning("❌ UserId not found in JWT claims"); } } catch (Exception ex) { logger.LogError(ex, "❌ Failed to extract UserId from JWT token"); } } await next(context); }); app.MapControllers(); // SignalR Hub app.MapHub("/hubs/entitychanges"); app.MapGet("/health", () => Results.Ok(new { status = "OK", timestamp = DateTime.UtcNow })) .AllowAnonymous(); app.Run(); if (app.Environment.IsProduction()) { Log.CloseAndFlush(); } // for testing purposes public partial class Program { }