using System.Text.Encodings.Web; using System.Text.Unicode; using JobsMedical.Web.Data; using JobsMedical.Web.Services; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); // Interest tracking + recommendation engine. builder.Services.AddHttpContextAccessor(); builder.Services.AddMemoryCache(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Listing parser: heuristic now; swap for an LLM-backed IListingParser later. builder.Services.AddSingleton(); // Scrape/ingestion engine: pluggable sources → dedupe → parse → validate → (AI audit) → publish/queue. builder.Services.AddHttpClient("scrape", c => { c.Timeout = TimeSpan.FromSeconds(20); c.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; HamkadrBot/1.0)"); }); builder.Services.AddHttpClient("ai"); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddHostedService(); // Phone-OTP cookie auth. builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(o => { o.LoginPath = "/Account/Login"; o.AccessDeniedPath = "/Account/Login"; o.ExpireTimeSpan = TimeSpan.FromDays(30); o.SlidingExpiration = true; }); // Emit Persian/Arabic characters directly in HTML instead of \u-style entities. builder.Services.AddSingleton(HtmlEncoder.Create( UnicodeRanges.BasicLatin, UnicodeRanges.Arabic, UnicodeRanges.ArabicSupplement, UnicodeRanges.ArabicExtendedA, UnicodeRanges.GeneralPunctuation)); builder.Services.AddDbContext(opt => opt.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); var app = builder.Build(); // Apply migrations + seed on startup (fine for MVP single-instance deploy). using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); db.Database.Migrate(); // Production seeds reference data only (no demo facilities/shifts); dev seeds the full board. await SeedData.EnsureSeededAsync(db, app.Environment.IsDevelopment()); } // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } // Behind nginx (TLS terminated upstream): trust X-Forwarded-Proto/For so the app knows it's // HTTPS — required for correct secure cookies and to avoid HTTPS-redirect loops. var forwardedOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }; forwardedOptions.KnownIPNetworks.Clear(); // only nginx can reach the container's bound port forwardedOptions.KnownProxies.Clear(); app.UseForwardedHeaders(forwardedOptions); app.UseHttpsRedirection(); app.UseRouting(); // Assign every visitor a stable cookie id so we can track interest from the first visit. app.UseMiddleware(); app.UseAuthentication(); app.UseAuthorization(); app.MapStaticAssets(); app.MapRazorPages() .WithStaticAssets(); // Lightweight liveness probe for the deploy health-wait loop (and uptime checks). app.MapGet("/healthz", () => Results.Text("ok")); app.Run();