using Marco.Pms.DataAccess.Data; using Marco.Pms.Helpers; using Marco.Pms.Helpers.CacheHelper; using Marco.Pms.Helpers.Utility; using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Middleware; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Serilog; using System.Text; var builder = WebApplication.CreateBuilder(args); #region ======================= Service Configuration (Dependency Injection) ======================= #region Logging // Add Serilog Configuration string? mongoConn = builder.Configuration["MongoDB:SerilogDatabaseUrl"]; string timeString = "00:00:30"; TimeSpan.TryParse(timeString, out TimeSpan timeSpan); builder.Host.UseSerilog((context, config) => { config.ReadFrom.Configuration(context.Configuration) .WriteTo.MongoDB( databaseUrl: mongoConn ?? string.Empty, collectionName: "api-logs", batchPostingLimit: 100, period: timeSpan ); }); #endregion #region CORS (Cross-Origin Resource Sharing) builder.Services.AddCors(options => { // A more permissive policy for development options.AddPolicy("DevCorsPolicy", policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .WithExposedHeaders("Authorization"); }); // A stricter policy for production (loaded from config) var corsSettings = builder.Configuration.GetSection("Cors"); var allowedOrigins = corsSettings.GetValue("AllowedOrigins")?.Split(',') ?? Array.Empty(); options.AddPolicy("ProdCorsPolicy", policy => { policy.WithOrigins(allowedOrigins) .AllowAnyMethod() .AllowAnyHeader(); }); }); #endregion #region Core Web & Framework Services builder.Services.AddControllers(); builder.Services.AddSignalR(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddHttpContextAccessor(); builder.Services.AddMemoryCache(); builder.Services.AddAutoMapper(typeof(Program)); builder.Services.AddHostedService(); #endregion #region Database & Identity string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString") ?? throw new InvalidOperationException("Database connection string 'DefaultConnectionString' not found."); // This single call correctly registers BOTH the DbContext (scoped) AND the IDbContextFactory (singleton). builder.Services.AddDbContextFactory(options => options.UseMySql(connString, ServerVersion.AutoDetect(connString))); builder.Services.AddDbContext(options => options.UseMySql(connString, ServerVersion.AutoDetect(connString))); builder.Services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); #endregion #region Authentication (JWT) var jwtSettings = builder.Configuration.GetSection("Jwt").Get() ?? throw new InvalidOperationException("JwtSettings section is missing or invalid."); if (jwtSettings != null && jwtSettings.Key != null) { builder.Services.AddSingleton(jwtSettings); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtSettings.Issuer, ValidAudience = jwtSettings.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)) }; // This event allows SignalR to get the token from the query string options.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs/marco")) { context.Token = accessToken; } return Task.CompletedTask; } }; }); } #endregion #region API Documentation (Swagger) builder.Services.AddSwaggerGen(option => { option.SwaggerDoc("v1", new OpenApiInfo { Title = "Marco PMS API", Version = "v1" }); option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter a valid token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "Bearer" }); option.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty() } }); }); #endregion #region Application-Specific Services // Configuration-bound services builder.Services.Configure(builder.Configuration.GetSection("SmtpSettings")); builder.Services.Configure(builder.Configuration.GetSection("AWS")); // Transient services (lightweight, created each time) builder.Services.AddTransient(); builder.Services.AddTransient(); // Scoped services (one instance per HTTP request) #region Customs Services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); #endregion #region Helpers builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); #endregion #region Cache Services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); #endregion // Singleton services (one instance for the app's lifetime) builder.Services.AddSingleton(); #endregion #region Web Server (Kestrel) builder.WebHost.ConfigureKestrel(options => { options.AddServerHeader = false; // Disable the "Server" header for security }); #endregion #endregion var app = builder.Build(); #region ===================== HTTP Request Pipeline Configuration ===================== // The order of middleware registration is critical for correct application behavior. #region Global Middleware (Run First) // These custom middleware components run at the beginning of the pipeline to handle cross-cutting concerns. app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); #endregion #region Development Environment Configuration // These tools are only enabled in the Development environment for debugging and API testing. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } #endregion #region Standard Middleware // Common middleware for handling static content, security, and routing. app.UseStaticFiles(); // Enables serving static files (e.g., from wwwroot) app.UseHttpsRedirection(); // Redirects HTTP requests to HTTPS #endregion #region Security (CORS, Authentication & Authorization) // Security-related middleware must be in the correct order. var corsPolicy = app.Environment.IsDevelopment() ? "DevCorsPolicy" : "ProdCorsPolicy"; app.UseCors(corsPolicy); // CORS must be applied before Authentication/Authorization. app.UseAuthentication(); // 1. Identifies who the user is. app.UseAuthorization(); // 2. Determines what the identified user is allowed to do. #endregion #region Endpoint Routing (Run Last) // These map incoming requests to the correct controller actions or SignalR hubs. app.MapControllers(); app.MapHub("/hubs/marco"); #endregion #endregion app.Run();