From 577acfd0ae6911b7954613767abca1cebef85153 Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 1 Feb 2026 14:04:56 +0330 Subject: [PATCH] change program.cs logger and docker file --- Dockerfile | 17 + ServiceHost/Program.cs | 722 ++++++++++++++++++----------------------- 2 files changed, 330 insertions(+), 409 deletions(-) diff --git a/Dockerfile b/Dockerfile index 94b8b168..f5749c81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,6 +72,23 @@ ENV TZ='Asia/Tehran' # Copy published app COPY --from=publish /app/publish . + +# ------------------------------------------------------------------- +# ✅ روش اصلاح شده و امن برای هندل کردن فایل‌های دارای فاصله (Space) +# ------------------------------------------------------------------- +RUN echo '#!/bin/bash' > /rename_script.sh && \ + echo 'find /app/wwwroot -depth -name "*[A-Z]*" -print0 | while IFS= read -r -d "" file; do' >> /rename_script.sh && \ + echo ' dir=$(dirname "$file")' >> /rename_script.sh && \ + echo ' base=$(basename "$file")' >> /rename_script.sh && \ + echo ' lower=$(echo "$base" | tr "[:upper:]" "[:lower:]")' >> /rename_script.sh && \ + echo ' if [ "$base" != "$lower" ]; then' >> /rename_script.sh && \ + echo ' mv -f "$file" "$dir/$lower"' >> /rename_script.sh && \ + echo ' fi' >> /rename_script.sh && \ + echo 'done' >> /rename_script.sh && \ + chmod +x /rename_script.sh && \ + /rename_script.sh && \ + rm /rename_script.sh + # Create directories for certificates, storage, faces, and logs # Note: Bind-mounted directories will override these, but we create them for consistency RUN mkdir -p /app/certs /app/Faces /app/Storage /app/Logs app/InsuranceList && \ diff --git a/ServiceHost/Program.cs b/ServiceHost/Program.cs index d924b849..dffa30bc 100644 --- a/ServiceHost/Program.cs +++ b/ServiceHost/Program.cs @@ -33,45 +33,22 @@ using GozareshgirProgramManager.Application.Modules.Users.Commands.CreateUser; using GozareshgirProgramManager.Infrastructure; using GozareshgirProgramManager.Infrastructure.Persistence.Seed; using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.OpenApi; using Serilog; using Serilog.Events; using ServiceHost.Hubs.ProgramManager; using ServiceHost.Notifications.ProgramManager; using ServiceHost.Conventions; using ServiceHost.Filters; +using Microsoft.Extensions.FileProviders; +using Microsoft.OpenApi; // Corrected using for PhysicalFileProvider // ==================================================================== -// ❌ FIX REMOVED: این بخش باعث می‌شد فایل‌های استاتیک در داکر پیدا نشوند +// ✅ BEST PRACTICE: Use two-stage Serilog initialization to log startup errors. // ==================================================================== -// Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", ""); -var builder = WebApplication.CreateBuilder(args); - -// ❌ این بخش هم حذف شد چون مانع لود شدن استاتیک فایل‌ها در پروداکشن می‌شود -// if (!builder.Environment.IsDevelopment()) -// { -// builder.Configuration["Microsoft.AspNetCore.Hosting.StaticWebAssets.UseStaticWebAssets"] = "false"; -// } - -builder.WebHost.ConfigureKestrel(serverOptions => { serverOptions.Limits.MaxRequestBodySize = long.MaxValue; }); - -builder.Services.AddRazorPages() - .AddRazorRuntimeCompilation(); - -//Register Services -//test -#region Register Services - -builder.Services.AddHttpContextAccessor(); -builder.Services.AddHttpClient("holidayApi", c => c.BaseAddress = new System.Uri("https://api.github.com")); -var connectionString = builder.Configuration.GetConnectionString("MesbahDb"); -var connectionStringTestDb = builder.Configuration.GetConnectionString("TestDb"); - -#region Serilog // Use Docker-compatible log path -var logDirectory = builder.Environment.IsDevelopment() - ? @"C:\Logs\Gozareshgir\" +var logDirectory = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development + ? @"C:\Logs\Gozareshgir\" : "/app/Logs"; if (!Directory.Exists(logDirectory)) @@ -79,415 +56,342 @@ if (!Directory.Exists(logDirectory)) Directory.CreateDirectory(logDirectory); } +// Bootstrap logger: Catches errors during host configuration Log.Logger = new LoggerConfiguration() - //NO EF Core log - .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning) - - //NO DbCommand log - .MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Warning) - - //NO Microsoft Public log - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - - //.MinimumLevel.Information() - .WriteTo.File( - path: Path.Combine(logDirectory, "gozareshgir_log.txt"), - rollingInterval: RollingInterval.Day, - retainedFileCountLimit: 30, - shared: true, - outputTemplate: - "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}" - ) - // ✅ Console logging برای Docker + .MinimumLevel.Information() .WriteTo.Console() - .CreateLogger(); + .CreateBootstrapLogger(); -#endregion +Log.Information("Starting web host..."); -builder.Services.AddProgramManagerApplication(); -builder.Services.AddProgramManagerInfrastructure(builder.Configuration); -builder.Services.AddValidatorsFromAssemblyContaining(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -#region MongoDb - -var mongoConnectionSection = builder.Configuration.GetSection("MongoDb"); -var mongoDbSettings = mongoConnectionSection.Get(); -var mongoClient = new MongoClient(mongoDbSettings.ConnectionString); -var mongoDatabase = mongoClient.GetDatabase(mongoDbSettings.DatabaseName); - -builder.Services.AddSingleton(mongoDatabase); - -#endregion - -builder.Services.AddSingleton, CustomJsonResultExecutor>(); -PersonalBootstrapper.Configure(builder.Services, connectionString); -TestDbBootStrapper.Configure(builder.Services, connectionStringTestDb); - -AccountManagementBootstrapper.Configure(builder.Services, connectionString); -WorkFlowBootstrapper.Configure(builder.Services, connectionString); -QueryBootstrapper.Configure(builder.Services); - - -builder.Services.AddSingleton(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); - -#region Mahan - -builder.Services.AddTransient(); -builder.Services.Configure(builder.Configuration); - -#endregion - -builder.Services.Configure(options => +try { - options.ValueCountLimit = int.MaxValue; - options.KeyLengthLimit = int.MaxValue; - options.ValueLengthLimit = int.MaxValue; - options.MultipartBodyLengthLimit = long.MaxValue; - options.MemoryBufferThreshold = int.MaxValue; - options.MultipartHeadersLengthLimit = int.MaxValue; -}); + var builder = WebApplication.CreateBuilder(args); -builder.Services.Configure(options => -{ - options.CheckConsentNeeded = context => true; -}); -var domain = builder.Configuration["Domain"]; - -builder.Services.ConfigureApplicationCookie(options => -{ - //options.Cookie.Name = "GozarAuth"; - options.Cookie.HttpOnly = true; - options.Cookie.SameSite = SameSiteMode.None; // مهم ✅ - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // فقط روی HTTPS کار می‌کنه ✅ - options.Cookie.Domain = domain; // دامنه مشترک بین پدر و ساب‌دامین‌ها ✅ -}); - - -builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) - .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o => - { - o.LoginPath = new PathString("/"); - o.LogoutPath = new PathString("/index"); - o.AccessDeniedPath = new PathString("/AccessDenied"); - - o.ExpireTimeSpan = TimeSpan.FromHours(10); - o.SlidingExpiration = true; - }); - -builder.Services.AddAuthorization(options => -{ - options.AddPolicy("AdminArea", - builder => builder.RequireClaim("AccountId")); - options.AddPolicy("AdminArea", - builder => builder.RequireClaim("AdminAreaPermission", new List { "true" })); -}); - -builder.Services.AddAuthorization(options => -{ - options.AddPolicy("ClientArea", - builder => builder.RequireClaim("AccountId")); - options.AddPolicy("ClientArea", - builder => builder.RequireClaim("ClientAriaPermission", new List { "true" })); -}); - -builder.Services.AddAuthorization(options => -{ - options.AddPolicy("CameraArea", - builder => builder.RequireClaim("AccountId")); -}); - -builder.Services.AddAuthorization(options => -{ - options.AddPolicy("AdminNewArea", - builder => builder.RequireClaim("AccountId")); - options.AddPolicy("AdminNewArea", - builder => builder.RequireClaim("AdminAreaPermission", new List { "true" })); -}); - - -builder.Services.AddControllers(options => - { - options.Conventions.Add(new ParameterBindingConvention()); - options.Filters.Add(new OperationResultFilter()); - }) - .AddJsonOptions(options => -{ - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); -}); - - -builder.Services.AddRazorPages(options => - options.Conventions.AuthorizeAreaFolder("Admin", "/", "AdminArea")); -builder.Services.AddRazorPages(options => - options.Conventions.AuthorizeAreaFolder("Client", "/", "ClientArea")) - .AddMvcOptions(options => options.Filters.Add()); -builder.Services.AddRazorPages(options => - options.Conventions.AuthorizeAreaFolder("Camera", "/", "CameraArea")); -builder.Services.AddRazorPages(options => - options.Conventions.AuthorizeAreaFolder("AdminNew", "/", "AdminNewArea")); -builder.Services.AddMvc(); -builder.Services.AddSignalR(); - -#endregion - -#region Swagger - -builder.Services.AddSwaggerGen(options => -{ - options.UseInlineDefinitionsForEnums(); - options.CustomSchemaIds(type => type.FullName); - - var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - options.IncludeXmlComments(xmlPath); - - var classLibraryXmlFile = "CompanyManagment.App.Contracts.xml"; - var classLibraryXmlPath = Path.Combine(AppContext.BaseDirectory, classLibraryXmlFile); - options.IncludeXmlComments(classLibraryXmlPath); - - - options.SwaggerDoc("General", new OpenApiInfo { Title = "API - General", Version = "v1" }); - options.SwaggerDoc("Admin", new OpenApiInfo { Title = "API - Admin", Version = "v1" }); - options.SwaggerDoc("Client", new OpenApiInfo { Title = "API - Client", Version = "v1" }); - options.SwaggerDoc("Camera", new OpenApiInfo { Title = "API - Camera", Version = "v1" }); - options.SwaggerDoc("ProgramManager", new OpenApiInfo { Title = "API - ProgramManager", Version = "v1" }); - - options.DocInclusionPredicate((docName, apiDesc) => - string.Equals(docName, apiDesc.GroupName, StringComparison.OrdinalIgnoreCase)); - - options.EnableAnnotations(); -}); - -#endregion - -#region CORS - -builder.Services.AddCors(options => -{ - options.AddPolicy("AllowSpecificOrigins", policy => - { - policy.WithOrigins( - "http://localhost:3000", - "http://localhost:4000", - "http://localhost:4001", - "http://localhost:4002", - "http://localhost:3001", - "https://gozareshgir.ir", - "https://dad-mehr.ir", - "https://admin.dad-mehr.ir", - "https://client.dad-mehr.ir", - "https://admin.gozareshgir.ir", - "https://client.gozareshgir.ir", - "https://admin.dadmehrg.ir", - "https://client.dadmehrg.ir", - "http://localhost:3300" - - ) - .AllowAnyHeader() - .AllowAnyMethod() - .AllowCredentials(); - }); -}); - -#endregion - -builder.Services.AddExceptionHandler(); - -var sepehrTerminalId = builder.Configuration.GetValue("SepehrGateWayTerminalId"); - -builder.Services.AddParbad().ConfigureGateways(gateways => -{ - gateways.AddSepehr().WithAccounts(accounts => - { - accounts.AddInMemory(account => - { - account.TerminalId = sepehrTerminalId; - account.Name="Sepehr Account"; - }); - }); -}).ConfigureHttpContext(httpContext=>httpContext.UseDefaultAspNetCore()) -.ConfigureStorage(storage => -{ - storage.UseMemoryCache(); -}); - - -// Configure Serilog for both Development and Production -builder.Host.UseSerilog((context, services, configuration) => -{ - var logConfig = configuration - .ReadFrom.Configuration(context.Configuration) + // ==================================================================== + // ✅ STANDARD SERILOG CONFIGURATION FOR PRODUCTION + // ==================================================================== + builder.Host.UseSerilog((context, services, configuration) => configuration + .ReadFrom.Configuration(context.Configuration) // Optional: Allows config from appsettings.json .ReadFrom.Services(services) - .Enrich.FromLogContext(); + .Enrich.FromLogContext() + .MinimumLevel.Information() // Default minimum level for your application's own logs + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) // Suppress noisy Microsoft logs + .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) // ✅ KEEP THIS: Shows "Now listening on..." + .MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Warning) // Suppresses EF query logs + .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.File( + path: Path.Combine(logDirectory, "gozareshgir_log.txt"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 30, + shared: true, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" + )); - logConfig.WriteTo.File( - path: Path.Combine(logDirectory, "gozareshgir_log.txt"), - rollingInterval: RollingInterval.Day, - retainedFileCountLimit: 30, - shared: true, - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}" - ); + builder.WebHost.ConfigureKestrel(serverOptions => { serverOptions.Limits.MaxRequestBodySize = long.MaxValue; }); - logConfig.WriteTo.Console(); + builder.Services.AddRazorPages() + .AddRazorRuntimeCompilation(); -}, writeToProviders: true); + #region Register Services -Log.Information("SERILOG STARTED SUCCESSFULLY"); + builder.Services.AddHttpContextAccessor(); + builder.Services.AddHttpClient("holidayApi", c => c.BaseAddress = new Uri("https://api.github.com")); + var connectionString = builder.Configuration.GetConnectionString("MesbahDb"); + var connectionStringTestDb = builder.Configuration.GetConnectionString("TestDb"); + builder.Services.AddProgramManagerApplication(); + builder.Services.AddProgramManagerInfrastructure(builder.Configuration); + builder.Services.AddValidatorsFromAssemblyContaining(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); -builder.Services.Configure(options => -{ - options.ForwardedHeaders = - ForwardedHeaders.XForwardedFor | - ForwardedHeaders.XForwardedProto; + #region MongoDb + var mongoConnectionSection = builder.Configuration.GetSection("MongoDb"); + var mongoDbSettings = mongoConnectionSection.Get(); + var mongoClient = new MongoClient(mongoDbSettings.ConnectionString); + var mongoDatabase = mongoClient.GetDatabase(mongoDbSettings.DatabaseName); + builder.Services.AddSingleton(mongoDatabase); + #endregion - var proxies = builder.Configuration["KNOWN_PROXIES"]; - if (!string.IsNullOrWhiteSpace(proxies)) + builder.Services.AddSingleton, CustomJsonResultExecutor>(); + PersonalBootstrapper.Configure(builder.Services, connectionString); + TestDbBootStrapper.Configure(builder.Services, connectionStringTestDb); + AccountManagementBootstrapper.Configure(builder.Services, connectionString); + WorkFlowBootstrapper.Configure(builder.Services, connectionString); + QueryBootstrapper.Configure(builder.Services); + + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + #region Mahan + builder.Services.AddTransient(); + builder.Services.Configure(builder.Configuration); + #endregion + + builder.Services.Configure(options => { - foreach (var proxy in proxies.Split(',', StringSplitOptions.RemoveEmptyEntries)) - { - options.KnownProxies.Add(IPAddress.Parse(proxy.Trim())); - } - } - -}); - - -var app = builder.Build(); -app.UseCors("AllowSpecificOrigins"); - -#region InternalProgarmManagerApi - -app.Use(async (context, next) => -{ - var host = context.Request.Host.Host?.ToLower() ?? ""; - - string baseUrl; - - if (host.Contains("localhost")) - baseUrl = builder.Configuration["InternalApi:Local"]; - else if (host.Contains("dadmehrg.ir")) - baseUrl = builder.Configuration["InternalApi:Dadmehrg"]; - else if (host.Contains("gozareshgir.ir")) - baseUrl = builder.Configuration["InternalApi:Gozareshgir"]; - else - baseUrl = builder.Configuration["InternalApi:Local"]; // fallback - - InternalApiCaller.SetBaseUrl(baseUrl); - - await next.Invoke(); -}); - -#endregion - -#region Mahan - -//the backend Tester -if (builder.Environment.IsDevelopment()) -{ - using var scope = app.Services.CreateScope(); - var tester = scope.ServiceProvider.GetRequiredService(); - await tester.Test(); -} - - -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(options => - { - options.DocExpansion(DocExpansion.None); - options.SwaggerEndpoint("/swagger/General/swagger.json", "API - General"); - options.SwaggerEndpoint("/swagger/Admin/swagger.json", "API - Admin"); - options.SwaggerEndpoint("/swagger/Client/swagger.json", "API - Client"); - options.SwaggerEndpoint("/swagger/Camera/swagger.json", "API - Camera"); - options.SwaggerEndpoint("/swagger/ProgramManager/swagger.json", "API - ProgramManager"); - + options.ValueCountLimit = int.MaxValue; + options.KeyLengthLimit = int.MaxValue; + options.ValueLengthLimit = int.MaxValue; + options.MultipartBodyLengthLimit = long.MaxValue; + options.MemoryBufferThreshold = int.MaxValue; + options.MultipartHeadersLengthLimit = int.MaxValue; }); -} -#endregion - -#region Create Http Pipeline - -app.UseForwardedHeaders(); - -if (builder.Environment.IsDevelopment()) -{ - app.UseDeveloperExceptionPage(); -} -else -{ - // The default HSTS value is 30 days. - app.UseHsts(); -} - -app.UseExceptionHandler(options => { }); - -//app.UseHttpsRedirection(); - -// ✅ FIX: UseStaticFiles must be BEFORE Routing and Auth -app.UseStaticFiles(); - -// Static files برای فایل‌های آپلود شده -var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Storage"); -if (!Directory.Exists(uploadsPath)) -{ - Directory.CreateDirectory(uploadsPath); -} - -app.UseStaticFiles(new StaticFileOptions -{ - FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadsPath), - RequestPath = "/storage", - OnPrepareResponse = ctx => + builder.Services.Configure(options => { - ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000"); + options.CheckConsentNeeded = context => true; + }); + + var domain = builder.Configuration["Domain"]; + builder.Services.ConfigureApplicationCookie(options => + { + options.Cookie.HttpOnly = true; + options.Cookie.SameSite = SameSiteMode.None; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + options.Cookie.Domain = domain; + }); + + builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o => + { + o.LoginPath = new PathString("/"); + o.LogoutPath = new PathString("/index"); + o.AccessDeniedPath = new PathString("/AccessDenied"); + o.ExpireTimeSpan = TimeSpan.FromHours(10); + o.SlidingExpiration = true; + }); + + builder.Services.AddAuthorization(options => + { + options.AddPolicy("AdminArea", builder => builder.RequireClaim("AccountId").RequireClaim("AdminAreaPermission", "true")); + options.AddPolicy("ClientArea", builder => builder.RequireClaim("AccountId").RequireClaim("ClientAriaPermission", "true")); + options.AddPolicy("CameraArea", builder => builder.RequireClaim("AccountId")); + options.AddPolicy("AdminNewArea", builder => builder.RequireClaim("AccountId").RequireClaim("AdminAreaPermission", "true")); + }); + + builder.Services.AddControllers(options => + { + options.Conventions.Add(new ParameterBindingConvention()); + options.Filters.Add(new OperationResultFilter()); + }) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + + builder.Services.AddRazorPages(options => + options.Conventions.AuthorizeAreaFolder("Admin", "/", "AdminArea")); + builder.Services.AddRazorPages(options => + options.Conventions.AuthorizeAreaFolder("Client", "/", "ClientArea")) + .AddMvcOptions(options => options.Filters.Add()); + builder.Services.AddRazorPages(options => + options.Conventions.AuthorizeAreaFolder("Camera", "/", "CameraArea")); + builder.Services.AddRazorPages(options => + options.Conventions.AuthorizeAreaFolder("AdminNew", "/", "AdminNewArea")); + builder.Services.AddMvc(); + builder.Services.AddSignalR(); + + #endregion + + #region Swagger + builder.Services.AddSwaggerGen(options => + { + options.UseInlineDefinitionsForEnums(); + options.CustomSchemaIds(type => type.FullName); + + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + options.IncludeXmlComments(xmlPath); + + var classLibraryXmlFile = "CompanyManagment.App.Contracts.xml"; + var classLibraryXmlPath = Path.Combine(AppContext.BaseDirectory, classLibraryXmlFile); + options.IncludeXmlComments(classLibraryXmlPath); + + options.SwaggerDoc("General", new OpenApiInfo { Title = "API - General", Version = "v1" }); + options.SwaggerDoc("Admin", new OpenApiInfo { Title = "API - Admin", Version = "v1" }); + options.SwaggerDoc("Client", new OpenApiInfo { Title = "API - Client", Version = "v1" }); + options.SwaggerDoc("Camera", new OpenApiInfo { Title = "API - Camera", Version = "v1" }); + options.SwaggerDoc("ProgramManager", new OpenApiInfo { Title = "API - ProgramManager", Version = "v1" }); + + options.DocInclusionPredicate((docName, apiDesc) => + string.Equals(docName, apiDesc.GroupName, StringComparison.OrdinalIgnoreCase)); + + options.EnableAnnotations(); + }); + #endregion + + #region CORS + builder.Services.AddCors(options => + { + options.AddPolicy("AllowSpecificOrigins", policy => + { + policy.WithOrigins( + "http://localhost:3000", "http://localhost:4000", "http://localhost:4001", + "http://localhost:4002", "http://localhost:3001", "https://gozareshgir.ir", + "https://dad-mehr.ir", "https://admin.dad-mehr.ir", "https://client.dad-mehr.ir", + "https://admin.gozareshgir.ir", "https://client.gozareshgir.ir", + "https://admin.dadmehrg.ir", "https://client.dadmehrg.ir", "http://localhost:3300" + ) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); + }); + #endregion + + builder.Services.AddExceptionHandler(); + + var sepehrTerminalId = builder.Configuration.GetValue("SepehrGateWayTerminalId"); + builder.Services.AddParbad().ConfigureGateways(gateways => + { + gateways.AddSepehr().WithAccounts(accounts => + { + accounts.AddInMemory(account => + { + account.TerminalId = sepehrTerminalId; + account.Name = "Sepehr Account"; + }); + }); + }).ConfigureHttpContext(httpContext => httpContext.UseDefaultAspNetCore()) + .ConfigureStorage(storage => + { + storage.UseMemoryCache(); + }); + + builder.Services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + var proxies = builder.Configuration["KNOWN_PROXIES"]; + if (!string.IsNullOrWhiteSpace(proxies)) + { + foreach (var proxy in proxies.Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + options.KnownProxies.Add(IPAddress.Parse(proxy.Trim())); + } + } + }); + + var app = builder.Build(); + + // ==================================================================== + // ✅ HTTP PIPELINE CONFIGURATION + // ==================================================================== + + app.UseCors("AllowSpecificOrigins"); + + #region InternalProgarmManagerApi + app.Use(async (context, next) => + { + var host = context.Request.Host.Host?.ToLower() ?? ""; + string baseUrl = host switch + { + var h when h.Contains("localhost") => builder.Configuration["InternalApi:Local"], + var h when h.Contains("dadmehrg.ir") => builder.Configuration["InternalApi:Dadmehrg"], + var h when h.Contains("gozareshgir.ir") => builder.Configuration["InternalApi:Gozareshgir"], + _ => builder.Configuration["InternalApi:Local"] + }; + InternalApiCaller.SetBaseUrl(baseUrl); + await next.Invoke(); + }); + #endregion + + #region Mahan + if (builder.Environment.IsDevelopment()) + { + using var scope = app.Services.CreateScope(); + var tester = scope.ServiceProvider.GetRequiredService(); + await tester.Test(); } -}); -// ✅ Routing comes after Static Files -app.UseRouting(); -app.UseWebSockets(); + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(options => + { + options.DocExpansion(DocExpansion.None); + options.SwaggerEndpoint("/swagger/General/swagger.json", "API - General"); + options.SwaggerEndpoint("/swagger/Admin/swagger.json", "API - Admin"); + options.SwaggerEndpoint("/swagger/Client/swagger.json", "API - Client"); + options.SwaggerEndpoint("/swagger/Camera/swagger.json", "API - Camera"); + options.SwaggerEndpoint("/swagger/ProgramManager/swagger.json", "API - ProgramManager"); + }); + } + #endregion -// ✅ Cookie Policy should be before Auth -app.UseCookiePolicy(); + app.UseForwardedHeaders(); -// ✅ Auth comes after Routing -app.UseAuthentication(); -app.UseAuthorization(); + if (builder.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseHsts(); + } + app.UseExceptionHandler(options => { }); -#region Mahan + app.Use(async (context, next) => + { + if (context.Request.Path.HasValue) + { + context.Request.Path = context.Request.Path.Value.ToLowerInvariant(); + } + await next(); + }); -//app.UseLoginHandlerMiddleware(); + app.UseStaticFiles(); -//app.UseCheckTaskMiddleware(); -app.UseMiddleware(); + var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Storage"); + if (!Directory.Exists(uploadsPath)) + { + Directory.CreateDirectory(uploadsPath); + } + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(uploadsPath), + RequestPath = "/storage", + OnPrepareResponse = ctx => + { + ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000"); + } + }); -#endregion + app.UseRouting(); + app.UseWebSockets(); + app.UseCookiePolicy(); + app.UseAuthentication(); + app.UseAuthorization(); + #region Mahan + app.UseMiddleware(); + #endregion -app.MapHub("/trackingHub"); -app.MapHub("/trackingSmsHub"); -app.MapHub("/trackingHolidayHub"); -app.MapHub("/trackingCheckoutHub"); -app.MapHub("/trackingSendSmsHub"); -app.MapHub("api/pm/board"); -app.MapRazorPages(); -app.MapControllers(); + app.MapHub("/trackingHub"); + app.MapHub("/trackingSmsHub"); + app.MapHub("/trackingHolidayHub"); + app.MapHub("/trackingCheckoutHub"); + app.MapHub("/trackingSendSmsHub"); + app.MapHub("api/pm/board"); + app.MapRazorPages(); + app.MapControllers(); -// Health check endpoint for Docker -app.MapGet("/health", () => Results.Ok(new { status = "Healthy", timestamp = DateTime.UtcNow })); + app.MapGet("/health", () => Results.Ok(new { status = "Healthy", timestamp = DateTime.UtcNow })); -#endregion - -app.Run(); + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "Host terminated unexpectedly"); +} +finally +{ + Log.CloseAndFlush(); +}