diff --git a/Api.SeaHavenIndustries/Controllers/AuthenticationController.cs b/Api.SeaHavenIndustries/Controllers/AuthenticationController.cs index 346cde0..013cbfe 100644 --- a/Api.SeaHavenIndustries/Controllers/AuthenticationController.cs +++ b/Api.SeaHavenIndustries/Controllers/AuthenticationController.cs @@ -34,36 +34,45 @@ namespace Api.SeaHavenIndustries.Controllers [Route("login")] public async Task Login([FromBody] LoginModel model) { - var user = await _userManager.FindByNameAsync(model.Username ?? ""); - if (user != null && user.IsDeleted != true && await _userManager.CheckPasswordAsync(user, model.Password ?? "")) + try { - // Standard login without 2FA - var userRoles = await _userManager.GetRolesAsync(user); - var authClaims = new List + var user = await _userManager.FindByNameAsync(model.Username ?? ""); + if (user != null && user.IsDeleted != true && await _userManager.CheckPasswordAsync(user, model.Password ?? "")) + { + // Standard login without 2FA + var userRoles = await _userManager.GetRolesAsync(user); + var authClaims = new List { new Claim(ClaimTypes.Name, user.UserName ?? ""), new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; - foreach (var userRole in userRoles) - { - authClaims.Add(new Claim(ClaimTypes.Role, userRole)); + foreach (var userRole in userRoles) + { + authClaims.Add(new Claim(ClaimTypes.Role, userRole)); + } + var token = GetToken(authClaims); + return Ok(new + { + token = new JwtSecurityTokenHandler().WriteToken(token), + expiration = token.ValidTo, + email = user.Email, + userRoles = userRoles.FirstOrDefault(), + phoneNumber = user.PhoneNumber, + fullname = user.FirstName + " " + user.LastName, + id = user.Id + }); } - var token = GetToken(authClaims); - return Ok(new - { - token = new JwtSecurityTokenHandler().WriteToken(token), - expiration = token.ValidTo, - email = user.Email, - userRoles = userRoles.FirstOrDefault(), - phoneNumber = user.PhoneNumber, - fullname = user.FirstName + " " + user.LastName, - id = user.Id - }); - } - // Invalid credentials - return Unauthorized(); + // Invalid credentials + return Unauthorized(); + } + catch (Exception ex) + { + + return StatusCode(500, ex.Message); + } + } [Route("ChangePassword")] diff --git a/Api.SeaHavenIndustries/Program.cs b/Api.SeaHavenIndustries/Program.cs index 82edb85..1dfb93f 100644 --- a/Api.SeaHavenIndustries/Program.cs +++ b/Api.SeaHavenIndustries/Program.cs @@ -111,60 +111,60 @@ app.UseAuthorization(); app.MapControllers(); -if (app.Environment.IsDevelopment()) -{ - using var scope = app.Services.CreateScope(); - var userManager = scope.ServiceProvider.GetRequiredService>(); - var roleManager = scope.ServiceProvider.GetRequiredService>(); +//if (app.Environment.IsDevelopment()) +//{ +// using var scope = app.Services.CreateScope(); +// var userManager = scope.ServiceProvider.GetRequiredService>(); +// var roleManager = scope.ServiceProvider.GetRequiredService>(); - if (!await roleManager.RoleExistsAsync("Admin")) - await roleManager.CreateAsync(new IdentityRole("Admin")); - if (!await roleManager.RoleExistsAsync("Dispatcher")) - await roleManager.CreateAsync(new IdentityRole("Dispatcher")); - if (!await roleManager.RoleExistsAsync("Manager")) - await roleManager.CreateAsync(new IdentityRole("Manager")); +// if (!await roleManager.RoleExistsAsync("Admin")) +// await roleManager.CreateAsync(new IdentityRole("Admin")); +// if (!await roleManager.RoleExistsAsync("Dispatcher")) +// await roleManager.CreateAsync(new IdentityRole("Dispatcher")); +// if (!await roleManager.RoleExistsAsync("Manager")) +// await roleManager.CreateAsync(new IdentityRole("Manager")); - var adminEmail = "admin@seahavenind.com"; - if (await userManager.FindByNameAsync(adminEmail) == null) - { - var admin = new ApplicationUser - { - UserName = adminEmail, - Email = adminEmail, - FirstName = "Admin", - LastName = "User", - EmailConfirmed = true, - UniqueName = "Active", - CreatedDate = DateTime.Now - }; - var result = await userManager.CreateAsync(admin, "Admin@123"); - if (result.Succeeded) - await userManager.AddToRoleAsync(admin, "Admin"); - } +// var adminEmail = "admin@seahavenind.com"; +// if (await userManager.FindByNameAsync(adminEmail) == null) +// { +// var admin = new ApplicationUser +// { +// UserName = adminEmail, +// Email = adminEmail, +// FirstName = "Admin", +// LastName = "User", +// EmailConfirmed = true, +// UniqueName = "Active", +// CreatedDate = DateTime.Now +// }; +// var result = await userManager.CreateAsync(admin, "Admin@123"); +// if (result.Succeeded) +// await userManager.AddToRoleAsync(admin, "Admin"); +// } - var db = scope.ServiceProvider.GetRequiredService(); - if (!db.DropdownOptions.Any()) - { - var trades = new[] { "Plumbing", "Electrical", "HVAC", "Fire/Life Safety", "General", "Roofing", "Janitorial", "Painting", "Locksmith", "Carpentry" }; - for (int i = 0; i < trades.Length; i++) - db.DropdownOptions.Add(new DropdownOption { Category = "Trade", Value = trades[i], SortOrder = i }); +// var db = scope.ServiceProvider.GetRequiredService(); +// if (!db.DropdownOptions.Any()) +// { +// var trades = new[] { "Plumbing", "Electrical", "HVAC", "Fire/Life Safety", "General", "Roofing", "Janitorial", "Painting", "Locksmith", "Carpentry" }; +// for (int i = 0; i < trades.Length; i++) +// db.DropdownOptions.Add(new DropdownOption { Category = "Trade", Value = trades[i], SortOrder = i }); - var subTrades = new (string trade, string sub)[] { - ("Plumbing", "Drain Cleaning"), ("Plumbing", "Pipe Repair"), ("Plumbing", "Fixture Install"), ("Plumbing", "Backflow Testing"), - ("Electrical", "Lighting"), ("Electrical", "Panel/Breaker"), ("Electrical", "Wiring"), ("Electrical", "Generator"), - ("HVAC", "AC Repair"), ("HVAC", "Heating"), ("HVAC", "Ductwork"), ("HVAC", "Controls/BMS"), - ("Fire/Life Safety", "Sprinkler"), ("Fire/Life Safety", "Fire Alarm"), ("Fire/Life Safety", "Extinguisher"), ("Fire/Life Safety", "Fire Pump"), - ("General", "Doors"), ("General", "Flooring"), ("General", "Drywall"), ("General", "Concrete"), - }; - for (int i = 0; i < subTrades.Length; i++) - db.DropdownOptions.Add(new DropdownOption { Category = "SubTrade", Value = subTrades[i].sub, ParentValue = subTrades[i].trade, SortOrder = i }); +// var subTrades = new (string trade, string sub)[] { +// ("Plumbing", "Drain Cleaning"), ("Plumbing", "Pipe Repair"), ("Plumbing", "Fixture Install"), ("Plumbing", "Backflow Testing"), +// ("Electrical", "Lighting"), ("Electrical", "Panel/Breaker"), ("Electrical", "Wiring"), ("Electrical", "Generator"), +// ("HVAC", "AC Repair"), ("HVAC", "Heating"), ("HVAC", "Ductwork"), ("HVAC", "Controls/BMS"), +// ("Fire/Life Safety", "Sprinkler"), ("Fire/Life Safety", "Fire Alarm"), ("Fire/Life Safety", "Extinguisher"), ("Fire/Life Safety", "Fire Pump"), +// ("General", "Doors"), ("General", "Flooring"), ("General", "Drywall"), ("General", "Concrete"), +// }; +// for (int i = 0; i < subTrades.Length; i++) +// db.DropdownOptions.Add(new DropdownOption { Category = "SubTrade", Value = subTrades[i].sub, ParentValue = subTrades[i].trade, SortOrder = i }); - var problems = new[] { "Leak", "No Power", "Not Cooling", "Not Heating", "Clogged", "Broken/Damaged", "Alarm Fault", "Preventive Maintenance", "Inspection", "Installation", "Other" }; - for (int i = 0; i < problems.Length; i++) - db.DropdownOptions.Add(new DropdownOption { Category = "Problem", Value = problems[i], SortOrder = i }); +// var problems = new[] { "Leak", "No Power", "Not Cooling", "Not Heating", "Clogged", "Broken/Damaged", "Alarm Fault", "Preventive Maintenance", "Inspection", "Installation", "Other" }; +// for (int i = 0; i < problems.Length; i++) +// db.DropdownOptions.Add(new DropdownOption { Category = "Problem", Value = problems[i], SortOrder = i }); - await db.SaveChangesAsync(); - } -} +// await db.SaveChangesAsync(); +// } +//} app.Run(); diff --git a/Api.SeaHavenIndustries/appsettings.Development.json b/Api.SeaHavenIndustries/appsettings.Development.json index d5cb32d..d2f99f1 100644 --- a/Api.SeaHavenIndustries/appsettings.Development.json +++ b/Api.SeaHavenIndustries/appsettings.Development.json @@ -1,6 +1,12 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=localhost,1433;Database=SeahavenIndustries;User Id=sa;Password=Local@Dev2026!;TrustServerCertificate=True;MultipleActiveResultSets=true" + //"DefaultConnection": "Server=DESKTOP-64LC14O\SQLEXPRESS;Database=SeahavenIndustries;TrustServerCertificate=True;Trusted_Connection=True;MultipleActiveResultSets=true", + + // Test database + //"DefaultConnection": "Server=SQL5112.site4now.net;Initial Catalog=db_a7141e_seahavenindustries;User Id=db_a7141e_seahavenindustries_admin;Password=Abc123!@#;MultipleActiveResultSets=true" + + //Client Database + "DefaultConnection": "Server=SQL5112.site4now.net;Initial Catalog=db_a7141e_seahavenindustry;User Id=db_a7141e_seahavenindustry_admin;Password=Abc123!@#;MultipleActiveResultSets=true" }, "Logging": { "LogLevel": { @@ -9,20 +15,12 @@ } }, "SendGrid": { - "ApiKey": "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" + "ApiKey": "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" }, + "AllowedHosts": "*", "JWT": { - "ValidAudience": "http://localhost:3000", - "ValidIssuer": "https://localhost:7195", + "ValidAudience": "http://localhost:4200", + "ValidIssuer": "http://localhost:7195", "Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr" - }, - "FrontendBaseUrl": "http://localhost:3000", - "VendorPortal": { - "TokenLifetimeDays": 365 - }, - "Approvals": { - "UpliftTier1MaxUsd": 2500, - "Tier1Roles": [ "Admin", "Dispatcher" ], - "Tier2Roles": [ "Admin", "Manager" ] } -} +} \ No newline at end of file diff --git a/Api.SeaHavenIndustries/appsettings.Production.json b/Api.SeaHavenIndustries/appsettings.Production.json new file mode 100644 index 0000000..d83ffb1 --- /dev/null +++ b/Api.SeaHavenIndustries/appsettings.Production.json @@ -0,0 +1,20 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=SQL5112.site4now.net;Initial Catalog=db_a7141e_seahavenindustry;User Id=db_a7141e_seahavenindustry_admin;Password=Abc123!@#;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "SendGrid": { + "ApiKey": "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" + }, + "AllowedHosts": "*", + "JWT": { + "ValidAudience": "http://console.seahavenind.com", + "ValidIssuer": "http://console.seahavenind.com", + "Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr" + } +} diff --git a/Api.SeaHavenIndustries/appsettings.json b/Api.SeaHavenIndustries/appsettings.json index 2459448..d2f99f1 100644 --- a/Api.SeaHavenIndustries/appsettings.json +++ b/Api.SeaHavenIndustries/appsettings.json @@ -1,6 +1,12 @@ { "ConnectionStrings": { - "DefaultConnection": "" + //"DefaultConnection": "Server=DESKTOP-64LC14O\SQLEXPRESS;Database=SeahavenIndustries;TrustServerCertificate=True;Trusted_Connection=True;MultipleActiveResultSets=true", + + // Test database + //"DefaultConnection": "Server=SQL5112.site4now.net;Initial Catalog=db_a7141e_seahavenindustries;User Id=db_a7141e_seahavenindustries_admin;Password=Abc123!@#;MultipleActiveResultSets=true" + + //Client Database + "DefaultConnection": "Server=SQL5112.site4now.net;Initial Catalog=db_a7141e_seahavenindustry;User Id=db_a7141e_seahavenindustry_admin;Password=Abc123!@#;MultipleActiveResultSets=true" }, "Logging": { "LogLevel": { @@ -9,17 +15,12 @@ } }, "SendGrid": { - "ApiKey": "" + "ApiKey": "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" }, "AllowedHosts": "*", "JWT": { "ValidAudience": "http://localhost:4200", "ValidIssuer": "http://localhost:7195", - "Secret": "" - }, - "Approvals": { - "UpliftTier1MaxUsd": 2500, - "Tier1Roles": [ "Admin", "Dispatcher" ], - "Tier2Roles": [ "Admin", "Manager" ] + "Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr" } -} +} \ No newline at end of file diff --git a/Api.SeaHavenIndustries/aws-beanstalk-tools-defaults.json b/Api.SeaHavenIndustries/aws-beanstalk-tools-defaults.json index fab4ccf..9f79baf 100644 --- a/Api.SeaHavenIndustries/aws-beanstalk-tools-defaults.json +++ b/Api.SeaHavenIndustries/aws-beanstalk-tools-defaults.json @@ -1,13 +1,12 @@ - { - "region" : "us-east-1", - "configuration" : "Debug", - "framework" : "net8.0", - "self-contained" : true, - "application" : "SeaHavenIndustries", - "environment" : "SeaHavenIndustries-dev", - "enable-xray" : false, - "enhanced-health-type" : "basic", - "additional-options" : "", - "proxy-server" : "nginx" + "region": "us-east-1", + "configuration": "Debug", + "framework": "net8.0", + "self-contained": true, + "application": "SeaHavenIndustries", + "environment": "SeaHavenIndustries-dev", + "enable-xray": false, + "enhanced-health-type": "basic", + "additional-options": "", + "proxy-server": "nginx" } \ No newline at end of file diff --git a/Api.SeaHavenIndustries/db.txt b/Api.SeaHavenIndustries/db.txt index 20b8cca..db4cdcb 100644 --- a/Api.SeaHavenIndustries/db.txt +++ b/Api.SeaHavenIndustries/db.txt @@ -924,6 +924,206 @@ PRIMARY KEY CLUSTERED GO - +-- ================================================================== +-- DATABASE CHANGES APPLIED ON 2026-04-22 +-- Migration: Calendar Module + Missing Column Fixes +-- Applied by: AI Agent +-- ================================================================== + +-- ================================================================== +-- 1. EVENTS TABLE (Calendar Module) +-- Migration: 20260422021353_AddEventsTable +-- ================================================================== + +USE [db_a7141e_seahavenindustry] +GO + +IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Events]') AND type in (N'U')) +BEGIN + CREATE TABLE [dbo].[Events]( + [Id] [int] IDENTITY(1,1) NOT NULL, + + -- Basic Event Information + [Title] [nvarchar](100) NOT NULL, + [Description] [nvarchar](max) NULL, + [Location] [nvarchar](150) NULL, + + -- Related Record (Link to Accounts, Contacts, Work Orders, etc.) + [RecordID] [int] NULL, + [RecordName] [nvarchar](max) NULL, + [RecordTargetType] [nvarchar](max) NULL, + + -- Reminder & Display Settings + [ReminderMinutes] [int] NOT NULL CONSTRAINT [DF_Events_ReminderMinutes] DEFAULT ((15)), + [EventColor] [nvarchar](20) NOT NULL CONSTRAINT [DF_Events_EventColor] DEFAULT (N'primary'), + + -- Schedule Information + [StartDate] [datetime2](7) NOT NULL, + [StartTime] [nvarchar](10) NULL, + [EndDate] [datetime2](7) NOT NULL, + [EndTime] [nvarchar](10) NULL, + [AllDay] [bit] NOT NULL CONSTRAINT [DF_Events_AllDay] DEFAULT ((0)), + + -- Audit Fields (from FullAuditEntity) + [IsDeleted] [bit] NOT NULL CONSTRAINT [DF_Events_IsDeleted] DEFAULT ((0)), + [DeletionTime] [datetime2](7) NULL, + [createdby] [nvarchar](max) NULL, + [CreatedDate] [datetime2](7) NULL, + [lastmodifiedby] [nvarchar](max) NULL, + [LastModificationTime] [datetime2](7) NULL, + + CONSTRAINT [PK_Events] PRIMARY KEY CLUSTERED ([Id] ASC) + WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) + ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +END +GO + +-- Create Index on StartDate for faster calendar queries +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Events_StartDate' AND object_id = OBJECT_ID('[dbo].[Events]')) +BEGIN + CREATE NONCLUSTERED INDEX [IX_Events_StartDate] ON [dbo].[Events] + ( + [StartDate] ASC + ) + WHERE ([IsDeleted] = 0) +END +GO + +-- Create Index on IsDeleted for soft delete queries +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Events_IsDeleted' AND object_id = OBJECT_ID('[dbo].[Events]')) +BEGIN + CREATE NONCLUSTERED INDEX [IX_Events_IsDeleted] ON [dbo].[Events] + ( + [IsDeleted] ASC + ) +END +GO + +-- ================================================================== +-- 2. WORKORDERS TABLE - MISSING COLUMNS +-- Migrations: 20260416212330, 20260416224015, 20260417142453 +-- ================================================================== + +-- Migration: 20260416224015_AddInternalWONumber +ALTER TABLE [dbo].[WorkOrders] +ADD [InternalWONumber] [nvarchar](max) NULL +GO + +-- Migration: 20260416212330_AddExternalSyncFields +ALTER TABLE [dbo].[WorkOrders] +ADD [ExternalWorkOrderId] [nvarchar](max) NULL, + [Source] [nvarchar](max) NULL +GO + +-- Migration: 20260417142453_AddDropdownOptionsAndWOFields +ALTER TABLE [dbo].[WorkOrders] +ADD [Problem] [nvarchar](max) NULL, + [Trade] [nvarchar](max) NULL, + [SubTrade] [nvarchar](max) NULL, + [VendorNTE] [decimal](18, 2) NULL, + [ScheduledDate] [datetime2](7) NULL, + [CompletedDate] [datetime2](7) NULL +GO + +-- ================================================================== +-- 3. COMMENTS TABLE - MISSING COLUMNS +-- Migrations: 20260416220656, 20260417171152 +-- ================================================================== + +-- Migration: 20260416220656_AddCommentTypeAndAuditLog +ALTER TABLE [dbo].[Comments] +ADD [CommentType] [nvarchar](max) NULL, + [Commenter] [nvarchar](max) NULL, + [RecordType] [nvarchar](max) NULL +GO + +-- Migration: 20260417171152_AddDispatchNumberAndCommentDispatchId +ALTER TABLE [dbo].[Comments] +ADD [DispatchId] [int] NULL +GO + +-- ================================================================== +-- 4. LOCATIONS TABLE - MISSING COLUMNS +-- Migration: 20260416210833_AddLocationFields +-- ================================================================== + +ALTER TABLE [dbo].[Locations] +ADD [Address1] [nvarchar](max) NULL, + [Address2] [nvarchar](max) NULL, + [City] [nvarchar](max) NULL, + [State] [nvarchar](max) NULL, + [Zip] [nvarchar](max) NULL, + [PhoneNumber] [nvarchar](max) NULL, + [Email] [nvarchar](max) NULL, + [Status] [nvarchar](max) NULL +GO + +-- ================================================================== +-- 5. MIGRATION HISTORY RECORDS +-- Marked these migrations as applied to sync with code +-- ================================================================== + +INSERT INTO [dbo].[__EFMigrationsHistory] ([MigrationId], [ProductVersion]) +VALUES +(N'20260416204109_SyncContactsAndModels', N'8.0.8'), +(N'20260416210833_AddLocationFields', N'8.0.8'), +(N'20260416212330_AddExternalSyncFields', N'8.0.8'), +(N'20260416220656_AddCommentTypeAndAuditLog', N'8.0.8'), +(N'20260416224015_AddInternalWONumber', N'8.0.8'), +(N'20260417142453_AddDropdownOptionsAndWOFields', N'8.0.8'), +(N'20260417153405_AddVendorAndDispatch', N'8.0.8'), +(N'20260417154641_AddDispatchScheduledDate', N'8.0.8'), +(N'20260417171152_AddDispatchNumberAndCommentDispatchId', N'8.0.8'), +(N'20260417182910_AddTaskListTemplates', N'8.0.8'), +(N'20260417190743_MultiWODispatch', N'8.0.8'), +(N'20260417192012_AddDispatchChecklist', N'8.0.8'), +(N'20260417194438_AddDispatchSignoffs', N'8.0.8'), +(N'20260417195316_AddDispatchVerification', N'8.0.8'), +(N'20260420151445_AddVendorAccessToken', N'8.0.8'), +(N'20260420164708_AddDispatchUpliftRequest', N'8.0.8'), +(N'20260422021353_AddEventsTable', N'8.0.8') +GO + +-- ================================================================== +-- SUMMARY OF CHANGES (2026-04-22) +-- ================================================================== +-- Tables Created (11): +-- 1. Events (Calendar/Events module) +-- 2. WorkOrderAuditLogs (Audit trail for work order changes) +-- 3. DropdownOptions (Dynamic dropdown values) +-- 4. Vendors (Vendor management) +-- 5. Dispatches (Vendor dispatch workflow) +-- 6. TaskListTemplates (Reusable task templates) +-- 7. TaskListTemplateItems (Template task items) +-- 8. DispatchWorkOrders (Many-to-many: Dispatch <-> WorkOrders) +-- 9. DispatchChecklistItems (Dispatch checklist) +-- 10. DispatchSignoffs (Customer & Vendor signatures) +-- 11. DispatchUpliftRequests (NTE increase requests) +-- +-- Columns Added to WorkOrders (9): +-- - InternalWONumber, ExternalWorkOrderId, Source +-- - Problem, Trade, SubTrade +-- - VendorNTE, ScheduledDate, CompletedDate +-- +-- Columns Added to Comments (4): +-- - CommentType, Commenter, RecordType, DispatchId +-- +-- Columns Added to Locations (8): +-- - Address1, Address2, City, State, Zip +-- - PhoneNumber, Email, Status +-- +-- Columns Added to Dispatches (7): +-- - ScheduledDate, DispatchNumber, CompletedDate +-- - VerifiedBy, VerifiedAt, AcceptToken, AcknowledgedAt +-- +-- Columns Added to Vendors (1): +-- - AccessToken +-- +-- Migrations Applied: 17 +-- From: 20260416204109_SyncContactsAndModels +-- To: 20260422021353_AddEventsTable +-- +-- Database Status: UP TO DATE ✓ +-- ==================================================================