271 lines
9.4 KiB
C#

using Marco.Pms.CacheHelper;
using FirebaseAdmin;
using Google.Apis.Auth.OAuth2;
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<string>("AllowedOrigins")?.Split(',') ?? Array.Empty<string>();
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<StartupUserSeeder>();
#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<ApplicationDbContext>(options =>
options.UseMySql(connString, ServerVersion.AutoDetect(connString)));
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(connString, ServerVersion.AutoDetect(connString)));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
#endregion
#region Authentication (JWT)
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>()
?? 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<string>()
}
});
});
#endregion
#region Application-Specific Services
// Configuration-bound services
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("SmtpSettings"));
builder.Services.Configure<AWSSettings>(builder.Configuration.GetSection("AWS"));
// Transient services (lightweight, created each time)
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddTransient<S3UploadService>();
// Scoped services (one instance per HTTP request)
#region Customs Services
builder.Services.AddScoped<RefreshTokenService>();
builder.Services.AddScoped<PermissionServices>();
builder.Services.AddScoped<MasterDataService>();
builder.Services.AddScoped<ISignalRService, SignalRService>();
builder.Services.AddScoped<IProjectServices, ProjectServices>();
builder.Services.AddScoped<IExpensesService, ExpensesService>();
builder.Services.AddScoped<IMasterService, MasterService>();
builder.Services.AddScoped<IDirectoryService, DirectoryService>();
builder.Services.AddScoped<IFirebaseService, FirebaseService>();
#endregion
#region Helpers
builder.Services.AddScoped<GeneralHelper>();
builder.Services.AddScoped<UserHelper>();
builder.Services.AddScoped<RolesHelper>();
builder.Services.AddScoped<EmployeeHelper>();
builder.Services.AddScoped<ReportHelper>();
builder.Services.AddScoped<CacheUpdateHelper>();
builder.Services.AddScoped<FeatureDetailsHelper>();
builder.Services.AddScoped<UtilityMongoDBHelper>();
#endregion
#region Cache Services
builder.Services.AddScoped<ProjectCache>();
builder.Services.AddScoped<EmployeeCache>();
builder.Services.AddScoped<ReportCache>();
builder.Services.AddScoped<ExpenseCache>();
builder.Services.AddScoped<SidebarMenuHelper>();
#endregion
// Singleton services (one instance for the app's lifetime)
builder.Services.AddSingleton<ILoggingService, LoggingService>();
string path = Path.Combine(builder.Environment.ContentRootPath, "FireBase", "service-account.json");
FirebaseApp.Create(new AppOptions()
{
Credential = GoogleCredential.FromFile(path),
});
#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<ExceptionHandlingMiddleware>();
app.UseMiddleware<TenantMiddleware>();
app.UseMiddleware<LoggingMiddleware>();
#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<MarcoHub>("/hubs/marco");
#endregion
#endregion
app.Run();