using EnvelopeGenerator.Server.Components; using EnvelopeGenerator.Server.Models; using EnvelopeGenerator.Server.Options; using DevExpress.Blazor; using EnvelopeGenerator.Server.Client.Services; using DigitalData.Core.API; using DigitalData.Core.Application; using EnvelopeGenerator.Infrastructure; using EnvelopeGenerator.Domain.Constants; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Localization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using System.Globalization; using Scalar.AspNetCore; using Microsoft.OpenApi.Models; using DigitalData.UserManager.DependencyInjection; using EnvelopeGenerator.Application; using DigitalData.Auth.Client; using DigitalData.Core.Abstractions; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using DigitalData.Core.Abstractions.Security.Extensions; using NLog.Web; using NLog; using DigitalData.Auth.Claims; using EnvelopeGenerator.Server; using DigitalData.EmailProfilerDispatcher; using EnvelopeGenerator.Application.Common.Interfaces.Doc; using EnvelopeGenerator.Infrastructure.Doc; var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); logger.Info("EnvelopeGenerator.Server logging initialized!"); try { var builder = WebApplication.CreateBuilder(args); // Load YARP configuration from yarp.json builder.Configuration.AddJsonFile("yarp.json", optional: true, reloadOnChange: true); builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); if (!builder.Environment.IsDevelopment()) { builder.Logging.ClearProviders(); builder.Host.UseNLog(); } var config = builder.Configuration; var deferredProvider = new DeferredServiceProvider(); // Add Blazor services builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); // Add API Controllers builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; }); builder.Services.AddHttpClient(); // YARP Reverse Proxy (for forwarding auth requests to AuthHub) builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); // HttpContextAccessor needed for SSR HttpClient configuration builder.Services.AddHttpContextAccessor(); // Named HttpClient for internal API calls // SenderAuthCookieHandler forwards the browser's Cookie header so that // Blazor Server components can call cookie-authenticated endpoints (AuthScheme.Sender). builder.Services.AddTransient(); builder.Services.AddHttpClient("EnvelopeGenerator.Server", (sp, client) => { var httpContextAccessor = sp.GetRequiredService(); var request = httpContextAccessor.HttpContext?.Request; if (request != null) { // Set base address to current host for SSR scenarios client.BaseAddress = new Uri($"{request.Scheme}://{request.Host}"); } }) .AddHttpMessageHandler(); // CORS Policy var allowedOrigins = config.GetSection("AllowedOrigins").Get() ?? throw new InvalidOperationException("AllowedOrigins section is missing in the configuration."); builder.Services.AddCors(options => { options.AddPolicy("AllowSpecificOriginsPolicy", builder => { builder.WithOrigins(allowedOrigins) .SetIsOriginAllowedToAllowWildcardSubdomains() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); }); // Swagger/OpenAPI builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "signFLOW Absender-API", Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschl�gen in der signFLOW-Anwendung.", Contact = new OpenApiContact { Name = "Digital Data GmbH", Url = new Uri("https://digitaldata.works/digitale-signatur#kontakt"), Email = "info-flow@digitaldata.works" }, }); options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", In = ParameterLocation.Header, Description = "JWT-Autorisierungs-Header unter Verwendung des Bearer-Schemas.", }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty() } }); var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml"); foreach (var xmlFile in xmlFiles) { options.IncludeXmlComments(xmlFile); } }); // Database Context var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue("UseDbMigration"); var cnnStrName = useDbMigration ? "DbMigrationTest" : "Default"; var connStr = config.GetConnectionString(cnnStrName) ?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration."); builder.Services.Configure(cs => cs.Value = connStr); builder.Services.AddDbContext(options => options.UseSqlServer(connStr)); // Authentication - AuthHub builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams")); var authTokenKeys = config.GetOrDefault(); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(AuthScheme.Sender, opt => { opt.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) => { var clientParams = deferredProvider.GetOptions(); var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience); return [publicKey.SecurityKey]; }, ValidateIssuer = true, ValidIssuer = authTokenKeys.Issuer, ValidateAudience = true, ValidAudience = authTokenKeys.Audience, }; opt.Events = new JwtBearerEvents { OnMessageReceived = context => { if (context.Token is null) { if (context.Request.Cookies.TryGetValue(authTokenKeys.Cookie, out var cookieToken) && cookieToken is not null) context.Token = cookieToken; else if (context.Request.Query.TryGetValue(authTokenKeys.QueryString, out var queryStrToken)) context.Token = queryStrToken; } return Task.CompletedTask; } }; }) .AddJwtBearer(AuthScheme.Receiver, opt => { opt.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) => { var clientParams = deferredProvider.GetOptions(); var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience); return [publicKey.SecurityKey]; }, ValidateIssuer = true, ValidIssuer = authTokenKeys.Issuer, ValidateAudience = true, ValidAudience = authTokenKeys.Audience, }; opt.Events = new JwtBearerEvents { OnMessageReceived = context => { var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries); var envelopeKey = paths?.LastOrDefault(); if (envelopeKey is not null) { var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey); if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken) && cookieToken is not null) context.Token = cookieToken; } return Task.CompletedTask; }, OnTokenValidated = context => { var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries); var envelopeKey = paths?.LastOrDefault(); var sub = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? context.Principal?.FindFirst("sub")?.Value; if (envelopeKey is null || sub != envelopeKey) context.Fail("Envelope key in the path does not match the token subject."); return Task.CompletedTask; } }; }); // Cookie Authentication builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.SameSite = SameSiteMode.Strict; options.LoginPath = "/api/auth/login"; options.LogoutPath = "/api/auth/logout"; options.SlidingExpiration = true; }); // Authorization Policies builder.Services.AddAuthorizationBuilder() .AddPolicy(AuthPolicy.Sender, policy => policy .RequireRole(Role.Sender) .AddAuthenticationSchemes(AuthScheme.Sender)) .AddPolicy(AuthPolicy.Receiver, policy => policy .AddAuthenticationSchemes(AuthScheme.Receiver) .RequireAuthenticatedUser() .RequireRole(Role.Receiver.Full, "receiver")) .AddPolicy(AuthPolicy.ReceiverTFA, policy => policy.RequireRole(Role.Receiver.TFA)) // Combined policy: accepts both Sender and Receiver tokens. // Used by endpoints accessible to both roles (e.g. UserPreferences). .AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy .AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver) .RequireAuthenticatedUser()); // User Manager #pragma warning disable CS0618 builder.Services.AddUserManager(); #pragma warning restore CS0618 // LDAP Directory Search builder.ConfigureBySection(); builder.Services.AddDirectorySearchService(config.GetSection("DirectorySearchOptions")); // Localization builder.Services.AddCookieBasedLocalizer(); // Cache options builder.Services.Configure(config.GetSection(CacheOptions.SectionName)); // Distributed Cache - SQL Server builder.Services.AddDistributedSqlServerCache(options => { config.GetSection("Cache:SqlServer").Bind(options); if (string.IsNullOrWhiteSpace(options.ConnectionString)) { options.ConnectionString = connStr; } }); // Envelope Generator Infrastructure & Application Services #pragma warning disable CS0618 builder.Services .AddEnvelopeGeneratorInfrastructureServices(opt => { opt.AddDbTriggerParams(config); opt.AddDbContext((provider, options) => { var logger = provider.GetRequiredService>(); options.UseSqlServer(connStr) .LogTo(log => logger.LogInformation("{log}", log), Microsoft.Extensions.Logging.LogLevel.Trace) .EnableSensitiveDataLogging() .EnableDetailedErrors(); }); opt.AddSQLExecutor(executor => executor.ConnectionString = connStr); }) .AddEnvelopeGeneratorServices(config); #pragma warning restore CS0618 builder.Services.AddDispatcher(); // User Preferences (theme, dark mode, grid layouts — stored in TBDD_CACHE) // Registration handled by Infrastructure DependencyInjection (AddEnvelopeGeneratorInfrastructureServices) // CustomImages — logo sources and heights, configurable per deployment via appsettings.json builder.Services.Configure( config.GetSection(EnvelopeGenerator.Server.Models.CustomImagesOptions.SectionName)); // Client-side services that MainLayout depends on must also be registered here // so that Blazor Auto prerender (SSR pass) can resolve them on the server. // ThemeService.InitializeAsync() uses IJSRuntime but is only called in OnAfterRenderAsync // (never during prerender), so registering it server-side is safe. builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Business Services (Server specific) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddDocInfrastructure(); builder.Services.AddScoped(); builder.Services.AddSingleton(); // EnvelopeService with HttpClient factory (for SSR scenarios) builder.Services.AddScoped(); // DocReceiverElementService (SignatureService alternative) builder.Services.AddScoped(); // SSR Authentication Service (for Envelope Receiver pages) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // DevExpress Server-Side Services (CRITICAL for DxPdfViewer) builder.Services.AddDevExpressBlazor(); builder.Services.AddDevExpressServerSideBlazorPdfViewer(); // PdfSharp font resolver — required for .NET 8 (no system font access without it) PdfSharp.Fonts.GlobalFontSettings.FontResolver = EnvelopeGenerator.Server.Services.PdfSharpFontResolver.Instance; // Configuration Options builder.Services.Configure( builder.Configuration.GetSection("ApiOptions")); builder.Services.Configure( builder.Configuration.GetSection("PdfViewerOptions")); var app = builder.Build(); deferredProvider.Factory = () => app.Services; // Exception handling middleware for API controllers app.UseMiddleware(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseWebAssemblyDebugging(); } else { app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseHsts(); } var useSwagger = app.Environment.IsDevelopment() || config.GetValue("UseSwagger"); var useScalar = app.Environment.IsDevelopment() || config.GetValue("UseScalar"); if (useSwagger) { app.UseSwagger(); app.UseSwaggerUI(); } if (useScalar) { if (!useSwagger) app.UseSwagger(); // Scalar requires the OpenAPI JSON endpoint app.MapScalarApiReference(); } // Set CORS policy app.UseCors("AllowSpecificOriginsPolicy"); // Localization (default German; can be switched via cookie) var defaultCulture = new CultureInfo("de-DE"); IList list = [ defaultCulture, new CultureInfo("en-US"), new CultureInfo("fr-FR") ]; var requestLocalizationOptions = new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture(defaultCulture), SupportedCultures = list, SupportedUICultures = list, RequestCultureProviders = [new CookieRequestCultureProvider()] }; requestLocalizationOptions.ApplyCurrentCultureToResponseHeaders = true; app.UseRequestLocalization(requestLocalizationOptions); app.UseHttpsRedirection(); app.UseDefaultFiles(); app.UseStaticFiles(); app.UseAntiforgery(); app.UseAuthentication(); app.UseAuthorization(); // API Controllers (map before Blazor routing) app.MapControllers(); // YARP Reverse Proxy - forwards unmatched requests to configured backends app.MapReverseProxy(); // Blazor routing app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(EnvelopeGenerator.Server.Client._Imports).Assembly); app.Run(); } catch (Exception ex) { logger.Error(ex, "Stopped program because of exception"); throw; }