275 lines
9.2 KiB
C#
275 lines
9.2 KiB
C#
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<EntityChangeInterceptor>();
|
|
|
|
builder.Services.AddDbContext<AppDbContext>((serviceProvider, options) =>
|
|
{
|
|
options.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly("DiunaBI.Infrastructure"));
|
|
|
|
// Only log SQL parameters in development (may contain sensitive data)
|
|
if (builder.Environment.IsDevelopment())
|
|
{
|
|
options.EnableSensitiveDataLogging();
|
|
}
|
|
|
|
// Add EntityChangeInterceptor
|
|
var interceptor = serviceProvider.GetRequiredService<EntityChangeInterceptor>();
|
|
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<GoogleAuthService>();
|
|
builder.Services.AddScoped<JwtTokenService>();
|
|
|
|
// Google Sheets dependencies
|
|
Console.WriteLine("Adding Google Sheets dependencies...");
|
|
builder.Services.AddSingleton<GoogleSheetsHelper>();
|
|
builder.Services.AddSingleton<GoogleDriveHelper>();
|
|
builder.Services.AddSingleton<SpreadsheetsResource.ValuesResource>(provider =>
|
|
{
|
|
var googleSheetsHelper = provider.GetRequiredService<GoogleSheetsHelper>();
|
|
var valuesResource = googleSheetsHelper.Service?.Spreadsheets.Values;
|
|
|
|
if (valuesResource == null)
|
|
{
|
|
throw new InvalidOperationException("Google Sheets Service is not initialized properly");
|
|
}
|
|
|
|
return valuesResource;
|
|
});
|
|
|
|
builder.Services.AddSingleton<PluginManager>();
|
|
|
|
// Job Queue Services
|
|
builder.Services.AddScoped<JobSchedulerService>();
|
|
builder.Services.AddHostedService<JobWorkerService>();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Auto-apply migrations on startup
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
|
|
|
|
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<PluginManager>();
|
|
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<ILogger<Program>>();
|
|
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<ILogger<Program>>();
|
|
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 - Requires JWT authentication
|
|
app.MapHub<EntityChangeHub>("/hubs/entitychanges").RequireAuthorization();
|
|
|
|
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 { } |