using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using TaskTrackingSystem.Database.AppDbContextModels; using Microsoft.AspNetCore.Authentication.JwtBearer; using System.Text; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddHttpClient(); builder.Services.AddSignalR(); // CORS policy for WebApp builder.Services.AddCors(options => { options.AddPolicy("AllowWebApp", policy => { policy.WithOrigins("http://localhost:5247", "https://localhost:7176") .AllowCredentials() .AllowAnyHeader() .AllowAnyMethod(); }); }); builder.Services.AddHttpContextAccessor(); 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(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHostedService(); builder.Services.AddScoped(); builder.Services.AddScoped, PasswordHasher>(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddAuthorization(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", In = ParameterLocation.Header, Description = "Paste the JWT token from the login response. You can include or omit the 'Bearer ' prefix." }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty() } }); }); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { var jwtSettings = builder.Configuration.GetSection("JwtSettings"); var key = jwtSettings["Key"] ?? throw new InvalidOperationException("JwtSettings:Key must be configured."); options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtSettings["Issuer"] ?? throw new InvalidOperationException("JwtSettings:Issuer must be configured."), ValidAudience = jwtSettings["Audience"] ?? throw new InvalidOperationException("JwtSettings:Audience must be configured."), IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)) }; }); builder.Services.AddDbContext(opt => { opt.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")); }); var app = builder.Build(); await EnsureSeedDataAsync(app); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(options => { options.EnablePersistAuthorization(); }); } app.UseCors("AllowWebApp"); app.UseAuthentication(); app.UseAuthorization(); app.MapGet("/health", () => Results.Ok(new { status = "ok" })); app.MapControllers(); app.MapHub("/hubs/notifications"); app.Run(); static async System.Threading.Tasks.Task EnsureSeedDataAsync(WebApplication app) { using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await db.Database.EnsureCreatedAsync(); // In Supabase/Hugging Face, EnsureCreatedAsync() might skip table creation // because other schemas (auth, storage, etc.) contain tables. // We check if the 'Menus' table exists in the public schema, and force creation of tables if it doesn't. var databaseCreator = db.Database.GetService() as RelationalDatabaseCreator; if (databaseCreator != null) { var connection = db.Database.GetDbConnection(); var connectionOpened = false; if (connection.State != System.Data.ConnectionState.Open) { await db.Database.OpenConnectionAsync(); connectionOpened = true; } try { using var command = connection.CreateCommand(); command.CommandText = "SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = 'Menus');"; var tableExists = (bool)(await command.ExecuteScalarAsync() ?? false); if (!tableExists) { await databaseCreator.CreateTablesAsync(); } } finally { if (connectionOpened) { await db.Database.CloseConnectionAsync(); } } } await EnsureReportUpgradeSchemaAsync(db); var dashboardMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD"); if (dashboardMenu == null) { dashboardMenu = new Menu { MenuCode = "DASHBOARD", MenuName = "Dashboard", MenuUrl = null, Icon = "layout-grid", Visible = false, OrderNo = 0, IsDeleted = true, CreatedAt = DateTime.UtcNow }; db.Menus.Add(dashboardMenu); await db.SaveChangesAsync(); } else { dashboardMenu.MenuName = "Dashboard"; dashboardMenu.MenuUrl = null; dashboardMenu.Icon = "layout-grid"; dashboardMenu.Visible = false; dashboardMenu.OrderNo = 0; dashboardMenu.IsDeleted = true; dashboardMenu.UpdatedAt = DateTime.UtcNow; } var adminDashboardMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD_ADMIN"); if (adminDashboardMenu == null) { adminDashboardMenu = new Menu { MenuCode = "DASHBOARD_ADMIN", ParentMenuId = null, MenuName = "Dashboard", MenuUrl = "/dashboard", Icon = "layout-dashboard", Visible = true, OrderNo = 0, IsDeleted = false, CreatedAt = DateTime.UtcNow }; db.Menus.Add(adminDashboardMenu); } else { adminDashboardMenu.ParentMenuId = null; adminDashboardMenu.MenuName = "Dashboard"; adminDashboardMenu.MenuUrl = "/dashboard"; adminDashboardMenu.Icon = "layout-dashboard"; adminDashboardMenu.Visible = true; adminDashboardMenu.OrderNo = 0; adminDashboardMenu.IsDeleted = false; adminDashboardMenu.UpdatedAt = DateTime.UtcNow; } var managerDashboardMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD_MANAGER"); if (managerDashboardMenu == null) { managerDashboardMenu = new Menu { MenuCode = "DASHBOARD_MANAGER", ParentMenuId = null, MenuName = "Dashboard", MenuUrl = "/dashboard/manager", Icon = "layout-dashboard", Visible = true, OrderNo = 0, IsDeleted = false, CreatedAt = DateTime.UtcNow }; db.Menus.Add(managerDashboardMenu); } else { managerDashboardMenu.ParentMenuId = null; managerDashboardMenu.MenuName = "Dashboard"; managerDashboardMenu.MenuUrl = "/dashboard/manager"; managerDashboardMenu.Icon = "layout-dashboard"; managerDashboardMenu.Visible = true; managerDashboardMenu.OrderNo = 0; managerDashboardMenu.IsDeleted = false; managerDashboardMenu.UpdatedAt = DateTime.UtcNow; } var employeeDashboardMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD_EMPLOYEE"); if (employeeDashboardMenu == null) { employeeDashboardMenu = new Menu { MenuCode = "DASHBOARD_EMPLOYEE", ParentMenuId = null, MenuName = "Dashboard", MenuUrl = "/dashboard/employee", Icon = "layout-dashboard", Visible = true, OrderNo = 0, IsDeleted = false, CreatedAt = DateTime.UtcNow }; db.Menus.Add(employeeDashboardMenu); } else { employeeDashboardMenu.ParentMenuId = null; employeeDashboardMenu.MenuName = "Dashboard"; employeeDashboardMenu.MenuUrl = "/dashboard/employee"; employeeDashboardMenu.Icon = "layout-dashboard"; employeeDashboardMenu.Visible = true; employeeDashboardMenu.OrderNo = 0; employeeDashboardMenu.IsDeleted = false; employeeDashboardMenu.UpdatedAt = DateTime.UtcNow; } await db.SaveChangesAsync(); var adminRole = await db.Roles.FirstOrDefaultAsync(r => r.Name == "Admin" && !r.IsDeleted); if (adminRole != null) { var adminDashboardAccess = await db.RoleMenus.FirstOrDefaultAsync(rm => rm.RoleId == adminRole.Id && rm.MenuId == adminDashboardMenu.MenuId); if (adminDashboardAccess == null) { db.RoleMenus.Add(new RoleMenu { RoleId = adminRole.Id, MenuId = adminDashboardMenu.MenuId, IsDeleted = false, CreatedAt = DateTime.UtcNow }); } else { adminDashboardAccess.IsDeleted = false; adminDashboardAccess.UpdatedAt = DateTime.UtcNow; } } var managerRole = await db.Roles.FirstOrDefaultAsync(r => r.Name == "Manager" && !r.IsDeleted); if (managerRole != null) { var managerDashboardAccess = await db.RoleMenus.FirstOrDefaultAsync(rm => rm.RoleId == managerRole.Id && rm.MenuId == managerDashboardMenu.MenuId); if (managerDashboardAccess == null) { db.RoleMenus.Add(new RoleMenu { RoleId = managerRole.Id, MenuId = managerDashboardMenu.MenuId, IsDeleted = false, CreatedAt = DateTime.UtcNow }); } else { managerDashboardAccess.IsDeleted = false; managerDashboardAccess.UpdatedAt = DateTime.UtcNow; } } var employeeRole = await db.Roles.FirstOrDefaultAsync(r => r.Name == "Employee" && !r.IsDeleted); if (employeeRole != null) { var employeeDashboardAccess = await db.RoleMenus.FirstOrDefaultAsync(rm => rm.RoleId == employeeRole.Id && rm.MenuId == employeeDashboardMenu.MenuId); if (employeeDashboardAccess == null) { db.RoleMenus.Add(new RoleMenu { RoleId = employeeRole.Id, MenuId = employeeDashboardMenu.MenuId, IsDeleted = false, CreatedAt = DateTime.UtcNow }); } else { employeeDashboardAccess.IsDeleted = false; employeeDashboardAccess.UpdatedAt = DateTime.UtcNow; } } await db.SaveChangesAsync(); await EnsureIssueReportMenuAsync(db); // Soft-delete existing tasks that belong to deleted projects var orphanedTasks = await db.Tasks .Where(t => t.IsDeleted != true && t.Project.IsDeleted == true) .ToListAsync(); foreach (var task in orphanedTasks) { task.IsDeleted = true; task.UpdatedAt = DateTime.UtcNow; } if (orphanedTasks.Any()) { db.Tasks.UpdateRange(orphanedTasks); } await db.SaveChangesAsync(); } static async System.Threading.Tasks.Task EnsureReportUpgradeSchemaAsync(AppDbContext db) { await db.Database.ExecuteSqlRawAsync(@" ALTER TABLE ""Issues"" ADD COLUMN IF NOT EXISTS ""DelayReason"" character varying(300); ALTER TABLE ""Issues"" ADD COLUMN IF NOT EXISTS ""IsBlocked"" boolean NOT NULL DEFAULT FALSE; ALTER TABLE ""Issues"" ADD COLUMN IF NOT EXISTS ""BlockedBy"" character varying(200); ALTER TABLE ""Issues"" ADD COLUMN IF NOT EXISTS ""EscalationLevel"" integer NOT NULL DEFAULT 0; "); } static async System.Threading.Tasks.Task EnsureIssueReportMenuAsync(AppDbContext db) { var reportsMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "REPORTS" && !m.IsDeleted); var issueReportMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "REPORTS_ISSUES"); if (issueReportMenu == null) { issueReportMenu = new Menu { MenuCode = "REPORTS_ISSUES", ParentMenuId = reportsMenu?.MenuId, MenuName = "Issue Report", MenuUrl = "/reports/issues", Icon = "file-pen-line", Visible = true, OrderNo = 15, IsDeleted = false, CreatedAt = DateTime.UtcNow }; db.Menus.Add(issueReportMenu); await db.SaveChangesAsync(); } else { issueReportMenu.ParentMenuId = reportsMenu?.MenuId; issueReportMenu.MenuName = "Issue Report"; issueReportMenu.MenuUrl = "/reports/issues"; issueReportMenu.Icon = "file-pen-line"; issueReportMenu.Visible = true; issueReportMenu.OrderNo = 15; issueReportMenu.IsDeleted = false; issueReportMenu.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } var reportRoles = await db.Roles .Where(role => !role.IsDeleted && (role.Name == "Admin" || role.Name == "Manager" || role.Name == "Employee")) .ToListAsync(); foreach (var role in reportRoles) { var roleMenu = await db.RoleMenus.FirstOrDefaultAsync(rm => rm.RoleId == role.Id && rm.MenuId == issueReportMenu.MenuId); if (roleMenu == null) { db.RoleMenus.Add(new RoleMenu { RoleId = role.Id, MenuId = issueReportMenu.MenuId, IsDeleted = false, CreatedAt = DateTime.UtcNow }); } else { roleMenu.IsDeleted = false; roleMenu.UpdatedAt = DateTime.UtcNow; } } await db.SaveChangesAsync(); }