using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Cleanuparr.Api.Filters; using Cleanuparr.Api.Json; using Cleanuparr.Infrastructure.Health; using Cleanuparr.Infrastructure.Hubs; using Microsoft.AspNetCore.Http.Json; using System.Text; using Cleanuparr.Api.Middleware; using Microsoft.Extensions.Options; namespace Cleanuparr.Api.DependencyInjection; public static class ApiDI { public static IServiceCollection AddApiServices(this IServiceCollection services) { services.Configure(options => { options.SerializerOptions.PropertyNameCaseInsensitive = true; options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; options.SerializerOptions.TypeInfoResolver = new SensitiveDataResolver( options.SerializerOptions.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver()); }); // Make JsonSerializerOptions available for injection services.AddSingleton(sp => sp.GetRequiredService>().Value.SerializerOptions); // Add API-specific services services .AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; options.JsonSerializerOptions.TypeInfoResolver = new SensitiveDataResolver( options.JsonSerializerOptions.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver()); }); services.AddEndpointsApiExplorer(); // Add SignalR for real-time updates services .AddSignalR() .AddJsonProtocol(options => { options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; options.PayloadSerializerOptions.Converters.Add(new JsonStringEnumConverter()); options.PayloadSerializerOptions.TypeInfoResolver = new SensitiveDataResolver( options.PayloadSerializerOptions.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver()); }); // Add health status broadcaster services.AddHostedService(); return services; } public static WebApplication ConfigureApi(this WebApplication app) { ILogger logger = app.Services.GetRequiredService>(); // Enable compression app.UseResponseCompression(); // Serve static files without caching app.UseStaticFiles(new StaticFileOptions { OnPrepareResponse = ctx => NoCacheAttribute.Apply(ctx.Context.Response.Headers) }); // Add the global exception handling middleware first app.UseMiddleware(); // Block non-auth requests until setup is complete app.UseMiddleware(); app.UseCors("Any"); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); // Custom SPA fallback to inject base path app.MapFallback(async context => { var basePath = app.Configuration.GetValue("BASE_PATH") ?? "/"; // Normalize the base path (remove trailing slash if not root) if (basePath != "/" && basePath.EndsWith("/")) { basePath = basePath.TrimEnd('/'); } var webRoot = app.Environment.WebRootPath ?? Path.Combine(app.Environment.ContentRootPath, "wwwroot"); var indexPath = Path.Combine(webRoot, "index.html"); if (!File.Exists(indexPath)) { context.Response.StatusCode = 404; await context.Response.WriteAsync("index.html not found"); return; } var indexContent = await File.ReadAllTextAsync(indexPath); // Inject the base path into the HTML var scriptInjection = $@" "; // Insert the script right before the existing script tag indexContent = indexContent.Replace( "