using ElmahCore; using ElmahCore.Mvc; using Serilog; using System.Globalization; using Microsoft.AspNetCore.Localization; using Server; using Server.HealthChecks; using Server.Helper; using Server.Models; using Server.Services; using System.Text.Json.Serialization; using System.Configuration; using Microsoft.OpenApi; using Shared.Models; using Microsoft.AspNetCore.ResponseCompression; using System.Net; using System.Text; using Server.Migrations; using Microsoft.Data.Sqlite; var builder = WebApplication.CreateBuilder(args); // Add Controllers with views & string conversion for enums builder.Services.AddControllersWithViews() .AddJsonOptions(options => { options.JsonSerializerOptions.Converters.Add( new JsonStringEnumConverter() ); }); // Add Configuration IConfigurationSection configurationSection = builder.Configuration.GetSection("Embeddingsearch"); EmbeddingSearchOptions configuration = configurationSection.Get() ?? throw new ConfigurationErrorsException("Unable to start server due to an invalid configration"); builder.Services.Configure(configurationSection); builder.Services.Configure(configurationSection); // Configure Kestrel builder.WebHost.ConfigureKestrel(options => { options.Limits.MaxRequestBodySize = configuration.MaxRequestBodySize ?? 50 * 1024 * 1024; }); // Migrate database var helper = new SQLHelper(new MySql.Data.MySqlClient.MySqlConnection(configuration.ConnectionStrings.SQL), configuration.ConnectionStrings.SQL); DatabaseMigrations.Migrate(helper); // Migrate SQLite cache if (configuration.ConnectionStrings.Cache is not null) { var SqliteConnection = new SqliteConnection(configuration.ConnectionStrings.Cache); SqliteConnection.Open(); SQLiteMigrations.Migrate(SqliteConnection); } // Add Localization builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); builder.Services.Configure(options => { var supportedCultures = new[] { new CultureInfo("en"), new CultureInfo("de") }; options.DefaultRequestCulture = new RequestCulture("en"); options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; }); // Add LocalizationService builder.Services.AddScoped(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApi(options => { options.AddDocumentTransformer((document, context, _) => { if (configuration.ApiKeys is null) return Task.CompletedTask; document.Components ??= new(); document.Components.SecuritySchemes ??= new Dictionary(); document.Components.SecuritySchemes["ApiKey"] = new OpenApiSecurityScheme { Type = SecuritySchemeType.ApiKey, Name = "X-API-KEY", In = ParameterLocation.Header, Description = "ApiKey must appear in header" }; document.Security ??= []; // Apply globally document.Security?.Add( new OpenApiSecurityRequirement { [new OpenApiSecuritySchemeReference("ApiKey", document)] = [] } ); return Task.CompletedTask; }); }); Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .CreateLogger(); builder.Logging.AddSerilog(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHealthChecks() .AddCheck("DatabaseHealthCheck", tags: ["Database"]) .AddCheck("AIProviderHealthCheck", tags: ["AIProvider"]); builder.Services.AddElmah(Options => { Options.OnPermissionCheck = context => context.User.Claims.Any(claim => claim.Value.Equals("Admin", StringComparison.OrdinalIgnoreCase) || claim.Value.Equals("Elmah", StringComparison.OrdinalIgnoreCase) ); Options.LogPath = configuration.Elmah?.LogPath ?? "~/logs"; }); builder.Services .AddAuthentication("AppCookie") .AddCookie("AppCookie", options => { options.LoginPath = "/Account/Login"; options.LogoutPath = "/Account/Logout"; options.AccessDeniedPath = "/Account/Denied"; }); builder.Services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin")); }); builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; options.Providers.Add(); options.Providers.Add(); options.MimeTypes = [ "text/plain", "text/css", "application/javascript", "text/javascript", "text/html", "application/xml", "text/xml", "application/json", "image/svg+xml" ]; }); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); // Configure Elmah app.Use(async (context, next) => { if (context.Request.Path.StartsWithSegments("/elmah")) { context.Response.OnStarting(() => { context.Response.Headers.Append( "Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'" ); return Task.CompletedTask; }); } await next(); }); app.Use(async (context, next) => { if (!context.Request.Path.StartsWithSegments("/elmah")) { await next(); return; } var originalBody = context.Response.Body; using var memStream = new MemoryStream(); context.Response.Body = memStream; await next(); memStream.Position = 0; var html = await new StreamReader(memStream).ReadToEndAsync(); if (context.Response.ContentType?.Contains("text/html") == true) { html = html.Replace( "", """ """ ); } var bytes = Encoding.UTF8.GetBytes(html); context.Response.ContentLength = bytes.Length; await originalBody.WriteAsync(bytes); context.Response.Body = originalBody; }); app.UseElmah(); app.MapHealthChecks("/healthz"); app.MapHealthChecks("/healthz/Database", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = c => c.Name.Contains("Database") }); app.MapHealthChecks("/healthz/AIProvider", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = c => c.Name.Contains("AIProvider") }); bool IsDevelopment = app.Environment.IsDevelopment(); app.Use(async (context, next) => { if (context.Request.Path.StartsWithSegments("/swagger")) { if (!context.User.Identity?.IsAuthenticated ?? true) { context.Response.Redirect($"/Account/Login?ReturnUrl={WebUtility.UrlEncode("/swagger")}"); return; } if (!context.User.IsInRole("Admin")) { context.Response.StatusCode = StatusCodes.Status403Forbidden; return; } } await next(); }); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/openapi/v1.json", "API v1"); options.RoutePrefix = "swagger"; options.EnablePersistAuthorization(); options.InjectStylesheet("/swagger-ui/custom.css"); options.InjectJavascript("/swagger-ui/custom.js"); }); app.MapOpenApi("/openapi/v1.json"); //app.UseElmahExceptionPage(); // Messes with JSON response for API calls. Leaving this here so I don't accidentally put this in again later on. if (configuration.ApiKeys is not null) { app.UseWhen(context => { RouteData routeData = context.GetRouteData(); string controllerName = routeData.Values["controller"]?.ToString() ?? "StaticFile"; if (controllerName == "Account" || controllerName == "Home" || controllerName == "StaticFile") { return false; } return true; }, appBuilder => { appBuilder.UseMiddleware(); }); } app.UseResponseCompression(); // Add localization var supportedCultures = new[] { "de", "de-DE", "en-US" }; var localizationOptions = new RequestLocalizationOptions() .SetDefaultCulture("de") .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); app.UseRequestLocalization(localizationOptions); app.MapControllers(); app.UseStaticFiles(new StaticFileOptions { OnPrepareResponse = ctx => { string requestPath = ctx.Context.Request.Path.ToString(); string[] cachedSuffixes = [".css", ".js", ".png", ".ico", ".woff2"]; if (cachedSuffixes.Any(suffix => requestPath.EndsWith(suffix))) { ctx.Context.Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromDays(365) }; } } }); app.Run();