diff --git a/TradeOffStackAPI.Tests/Integration/CustomWebApplicationFactory.cs b/TradeOffStackAPI.Tests/Integration/CustomWebApplicationFactory.cs index b0f7c54..029c0ad 100644 --- a/TradeOffStackAPI.Tests/Integration/CustomWebApplicationFactory.cs +++ b/TradeOffStackAPI.Tests/Integration/CustomWebApplicationFactory.cs @@ -17,12 +17,15 @@ namespace TradeOffStackAPI.Tests.Integration; public class CustomWebApplicationFactory : WebApplicationFactory where TProgram : class { - private readonly DbConnection _connection; + private readonly DbConnection _coreConnection; + private readonly DbConnection _assetConnection; public CustomWebApplicationFactory() { - _connection = new SqliteConnection("DataSource=:memory:"); - _connection.Open(); + _coreConnection = new SqliteConnection("DataSource=:memory:"); + _coreConnection.Open(); + _assetConnection = new SqliteConnection("DataSource=:memory:"); + _assetConnection.Open(); } protected override void ConfigureWebHost(IWebHostBuilder builder) @@ -55,22 +58,22 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.AddDbContext(options => { - options.UseSqlite(_connection); + options.UseSqlite(_coreConnection); }); services.AddScoped>(sp => new DbContextOptionsBuilder() - .UseSqlite(_connection) + .UseSqlite(_coreConnection) .Options); services.AddDbContext(options => { - options.UseSqlite(_connection); + options.UseSqlite(_assetConnection); }); services.AddScoped>(sp => new DbContextOptionsBuilder() - .UseSqlite(_connection) + .UseSqlite(_assetConnection) .Options); var sp = services.BuildServiceProvider(); @@ -87,8 +90,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) protected override void Dispose(bool disposing) { base.Dispose(disposing); - _connection.Close(); - _connection.Dispose(); + _coreConnection.Close(); + _coreConnection.Dispose(); + _assetConnection.Close(); + _assetConnection.Dispose(); } } diff --git a/TradeOffStackAPI.Tests/Integration/EquipmentIntegrationTests.cs b/TradeOffStackAPI.Tests/Integration/EquipmentIntegrationTests.cs index 0e51068..c67ef6e 100644 --- a/TradeOffStackAPI.Tests/Integration/EquipmentIntegrationTests.cs +++ b/TradeOffStackAPI.Tests/Integration/EquipmentIntegrationTests.cs @@ -155,6 +155,12 @@ public async Task GetEquipment_AsEmployee_ShouldSucceed() // Act var response = await client.GetAsync("/api/equipment"); + if (response.StatusCode != HttpStatusCode.OK) + { + var content = await response.Content.ReadAsStringAsync(); + Assert.Fail($"Request failed with status {response.StatusCode} and body: {content}"); + } + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } diff --git a/TradeOffStackAPI/Data/AssetDbContext.cs b/TradeOffStackAPI/Data/AssetDbContext.cs index 54481e0..f9795a8 100644 --- a/TradeOffStackAPI/Data/AssetDbContext.cs +++ b/TradeOffStackAPI/Data/AssetDbContext.cs @@ -25,7 +25,23 @@ public AssetDbContext(DbContextOptions options, ICurrentUserServ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - // Only apply configurations from this assembly that relate to AssetPortal - modelBuilder.ApplyConfigurationsFromAssembly(typeof(AssetDbContext).Assembly); + + // Register PostgreSQL Enums with PascalCase translator to match runtime config + var translator = new Npgsql.NameTranslation.NpgsqlNullNameTranslator(); + modelBuilder.HasPostgresEnum(name: "asset_status", nameTranslator: translator); + modelBuilder.HasPostgresEnum(name: "asset_category", nameTranslator: translator); + modelBuilder.HasPostgresEnum(name: "reservation_status", nameTranslator: translator); + modelBuilder.HasPostgresEnum(name: "maintenance_status", nameTranslator: translator); + modelBuilder.HasPostgresEnum(name: "maintenance_priority", nameTranslator: translator); + modelBuilder.HasPostgresEnum(name: "audit_action", nameTranslator: translator); + modelBuilder.HasPostgresEnum(name: "depreciation_method", nameTranslator: translator); + + // Register specific configurations to prevent configuration leakage between contexts + modelBuilder.ApplyConfiguration(new TradeOffStackAPI.Data.Configurations.EquipmentConfiguration()); + modelBuilder.ApplyConfiguration(new TradeOffStackAPI.Data.Configurations.ReservationConfiguration()); + modelBuilder.ApplyConfiguration(new TradeOffStackAPI.Data.Configurations.MaintenanceRequestConfiguration()); + modelBuilder.ApplyConfiguration(new TradeOffStackAPI.Configurations.SoftwareLicenseConfiguration()); + modelBuilder.ApplyConfiguration(new TradeOffStackAPI.Configurations.EquipmentLicenseConfiguration()); + modelBuilder.ApplyConfiguration(new TradeOffStackAPI.Data.Configurations.AuditLogConfiguration()); } } diff --git a/TradeOffStackAPI/Data/CoreDbContext.cs b/TradeOffStackAPI/Data/CoreDbContext.cs index d0dccce..146463b 100644 --- a/TradeOffStackAPI/Data/CoreDbContext.cs +++ b/TradeOffStackAPI/Data/CoreDbContext.cs @@ -17,9 +17,19 @@ public CoreDbContext(DbContextOptions options, ICurrentUserServic protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - // Only apply configurations from this assembly that relate to Core - // For simplicity, we just apply all configurations since EF ignores irrelevant ones usually, - // but it's safer to only apply specific ones if we had them separated. - modelBuilder.ApplyConfigurationsFromAssembly(typeof(CoreDbContext).Assembly); + + // Register PostgreSQL Enums with PascalCase translator to match runtime config + var translator = new Npgsql.NameTranslation.NpgsqlNullNameTranslator(); + modelBuilder.HasPostgresEnum(name: "user_role", nameTranslator: translator); + modelBuilder.HasPostgresEnum(name: "audit_action", nameTranslator: translator); + + // Ignore asset-related navigation properties on User for CoreDbContext + modelBuilder.Entity().Ignore(u => u.Reservations); + modelBuilder.Entity().Ignore(u => u.MaintenanceRequests); + + // Register specific configurations to prevent configuration leakage between contexts + modelBuilder.ApplyConfiguration(new Configurations.UserConfiguration()); + modelBuilder.ApplyConfiguration(new Configurations.DepartmentConfiguration()); + modelBuilder.ApplyConfiguration(new Configurations.AuditLogConfiguration()); } } \ No newline at end of file diff --git a/TradeOffStackAPI/Middleware/ExceptionHandlingMiddleware.cs b/TradeOffStackAPI/Middleware/ExceptionHandlingMiddleware.cs index 46a995b..b770751 100644 --- a/TradeOffStackAPI/Middleware/ExceptionHandlingMiddleware.cs +++ b/TradeOffStackAPI/Middleware/ExceptionHandlingMiddleware.cs @@ -58,7 +58,7 @@ private async Task HandleAsync(HttpContext context, Exception exception) Title = title, // CORRECTION : On utilise exception.ToString() en développement pour avoir tous les détails, // y compris les exceptions internes, ce qui est crucial pour le débogage. - Detail = _env.IsDevelopment() ? exception.ToString() : "An internal error occurred.", + Detail = (_env.IsDevelopment() || _env.IsEnvironment("Testing")) ? exception.ToString() : "An internal error occurred.", Instance = context.Request.Path }; diff --git a/TradeOffStackAPI/Migrations/20260610202709_AddSaaSArchitecture.cs b/TradeOffStackAPI/Migrations/20260610202709_AddSaaSArchitecture.cs deleted file mode 100644 index 8aacd59..0000000 --- a/TradeOffStackAPI/Migrations/20260610202709_AddSaaSArchitecture.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using TradeOffStackAPI.Models; - -#nullable disable - -namespace TradeOffStackAPI.Migrations -{ - /// - public partial class AddSaaSArchitecture : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "SaaSProviders", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "text", nullable: false), - WebsiteUrl = table.Column(type: "text", nullable: true), - Category = table.Column(type: "text", nullable: true), - ContactEmail = table.Column(type: "text", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SaaSProviders", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "SaaSSubscriptions", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ProviderId = table.Column(type: "uuid", nullable: false), - PlanName = table.Column(type: "text", nullable: false), - BillingCycle = table.Column(type: "text", nullable: false), - CostPerSeat = table.Column(type: "numeric", nullable: false), - TotalSeats = table.Column(type: "integer", nullable: false), - RenewalDate = table.Column(type: "timestamp with time zone", nullable: true), - Status = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SaaSSubscriptions", x => x.Id); - table.ForeignKey( - name: "FK_SaaSSubscriptions_SaaSProviders_ProviderId", - column: x => x.ProviderId, - principalTable: "SaaSProviders", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "SaaSAssignments", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - SubscriptionId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - AssignedDate = table.Column(type: "timestamp with time zone", nullable: false), - LastLoginDate = table.Column(type: "timestamp with time zone", nullable: true), - IsActive = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SaaSAssignments", x => x.Id); - table.ForeignKey( - name: "FK_SaaSAssignments_SaaSSubscriptions_SubscriptionId", - column: x => x.SubscriptionId, - principalTable: "SaaSSubscriptions", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_SaaSAssignments_SubscriptionId", - table: "SaaSAssignments", - column: "SubscriptionId"); - - migrationBuilder.CreateIndex( - name: "IX_SaaSSubscriptions_ProviderId", - table: "SaaSSubscriptions", - column: "ProviderId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "SaaSAssignments"); - - migrationBuilder.DropTable( - name: "SaaSSubscriptions"); - - migrationBuilder.DropTable( - name: "SaaSProviders"); - } - } -} diff --git a/TradeOffStackAPI/Migrations/20260610202709_AddSaaSArchitecture.Designer.cs b/TradeOffStackAPI/Migrations/20260612101305_InitialAsset.Designer.cs similarity index 79% rename from TradeOffStackAPI/Migrations/20260610202709_AddSaaSArchitecture.Designer.cs rename to TradeOffStackAPI/Migrations/20260612101305_InitialAsset.Designer.cs index f8af706..a6686bb 100644 --- a/TradeOffStackAPI/Migrations/20260610202709_AddSaaSArchitecture.Designer.cs +++ b/TradeOffStackAPI/Migrations/20260612101305_InitialAsset.Designer.cs @@ -13,8 +13,8 @@ namespace TradeOffStackAPI.Migrations { [DbContext(typeof(AssetDbContext))] - [Migration("20260610202709_AddSaaSArchitecture")] - partial class AddSaaSArchitecture + [Migration("20260612101305_InitialAsset")] + partial class InitialAsset { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -24,6 +24,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasAnnotation("ProductVersion", "8.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 63); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "asset_category", new[] { "None", "Laptop", "Monitor", "Peripheral", "NetworkDevice" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "asset_status", new[] { "None", "Available", "Reserved", "OutForRepair", "Retired" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "audit_action", new[] { "Created", "Updated", "Deleted" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "depreciation_method", new[] { "None", "StraightLine", "DecliningBalance" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "maintenance_priority", new[] { "Low", "Medium", "High", "Critical" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "maintenance_status", new[] { "Pending", "InProgress", "Completed", "Cancelled" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "reservation_status", new[] { "Pending", "Approved", "Rejected", "Active", "Returned", "Cancelled" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("TradeOffStackAPI.Models.AuditLog", b => @@ -71,36 +78,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("audit_logs", (string)null); }); - modelBuilder.Entity("TradeOffStackAPI.Models.Department", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasAnnotation("Relational:JsonPropertyName", "id"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasDefaultValueSql("CURRENT_TIMESTAMP") - .HasAnnotation("Relational:JsonPropertyName", "created_at"); - - b.Property("Description") - .HasColumnType("text") - .HasAnnotation("Relational:JsonPropertyName", "description"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)") - .HasAnnotation("Relational:JsonPropertyName", "name"); - - b.HasKey("Id"); - - b.ToTable("departments", (string)null); - - b.HasAnnotation("Relational:JsonPropertyName", "department"); - }); - modelBuilder.Entity("TradeOffStackAPI.Models.Equipment", b => { b.Property("Id") @@ -257,15 +234,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasAnnotation("Relational:JsonPropertyName", "technician_notes"); - b.Property("UserId") - .HasColumnType("uuid"); - b.HasKey("Id"); b.HasIndex("EquipmentId"); - b.HasIndex("UserId"); - b.ToTable("maintenance_requests", (string)null); }); @@ -327,8 +299,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("EquipmentId"); - b.HasIndex("UserId"); - b.ToTable("reservations", (string)null); }); @@ -490,80 +460,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasAnnotation("Relational:JsonPropertyName", "software_license"); }); - modelBuilder.Entity("TradeOffStackAPI.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasAnnotation("Relational:JsonPropertyName", "id"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasDefaultValueSql("CURRENT_TIMESTAMP") - .HasAnnotation("Relational:JsonPropertyName", "created_at"); - - b.Property("DepartmentId") - .HasColumnType("uuid") - .HasAnnotation("Relational:JsonPropertyName", "department_id"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)") - .HasAnnotation("Relational:JsonPropertyName", "email"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasAnnotation("Relational:JsonPropertyName", "first_name"); - - b.Property("IsActive") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true) - .HasAnnotation("Relational:JsonPropertyName", "is_active"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasAnnotation("Relational:JsonPropertyName", "last_name"); - - b.Property("PasswordHash") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("PhoneNumber") - .HasMaxLength(20) - .HasColumnType("character varying(20)") - .HasAnnotation("Relational:JsonPropertyName", "phone_number"); - - b.Property("ProfileImage") - .HasColumnType("text") - .HasAnnotation("Relational:JsonPropertyName", "profile_image"); - - b.Property("ProfileImageUrl") - .HasColumnType("text") - .HasAnnotation("Relational:JsonPropertyName", "profile_image_url"); - - b.Property("Role") - .HasColumnType("integer") - .HasColumnName("Role") - .HasAnnotation("Relational:JsonPropertyName", "role"); - - b.HasKey("Id"); - - b.HasIndex("DepartmentId"); - - b.HasIndex("Email") - .IsUnique(); - - b.ToTable("users", (string)null); - }); - modelBuilder.Entity("TradeOffStackAPI.Models.EquipmentLicense", b => { b.HasOne("TradeOffStackAPI.Models.Equipment", "Equipment") @@ -591,10 +487,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("TradeOffStackAPI.Models.User", null) - .WithMany("MaintenanceRequests") - .HasForeignKey("UserId"); - b.Navigation("Equipment"); }); @@ -606,12 +498,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("TradeOffStackAPI.Models.User", null) - .WithMany("Reservations") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - b.Navigation("Equipment"); }); @@ -637,21 +523,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Provider"); }); - modelBuilder.Entity("TradeOffStackAPI.Models.User", b => - { - b.HasOne("TradeOffStackAPI.Models.Department", "Department") - .WithMany("Users") - .HasForeignKey("DepartmentId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("Department"); - }); - - modelBuilder.Entity("TradeOffStackAPI.Models.Department", b => - { - b.Navigation("Users"); - }); - modelBuilder.Entity("TradeOffStackAPI.Models.Equipment", b => { b.Navigation("EquipmentLicenses"); @@ -675,13 +546,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Navigation("EquipmentLicenses"); }); - - modelBuilder.Entity("TradeOffStackAPI.Models.User", b => - { - b.Navigation("MaintenanceRequests"); - - b.Navigation("Reservations"); - }); #pragma warning restore 612, 618 } } diff --git a/TradeOffStackAPI/Migrations/20260612101305_InitialAsset.cs b/TradeOffStackAPI/Migrations/20260612101305_InitialAsset.cs new file mode 100644 index 0000000..b9579be --- /dev/null +++ b/TradeOffStackAPI/Migrations/20260612101305_InitialAsset.cs @@ -0,0 +1,284 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using TradeOffStackAPI.Models; + +#nullable disable + +namespace TradeOffStackAPI.Migrations +{ + /// + public partial class InitialAsset : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:asset_category", "None,Laptop,Monitor,Peripheral,NetworkDevice") + .Annotation("Npgsql:Enum:asset_status", "None,Available,Reserved,OutForRepair,Retired") + .Annotation("Npgsql:Enum:audit_action", "Created,Updated,Deleted") + .Annotation("Npgsql:Enum:depreciation_method", "None,StraightLine,DecliningBalance") + .Annotation("Npgsql:Enum:maintenance_priority", "Low,Medium,High,Critical") + .Annotation("Npgsql:Enum:maintenance_status", "Pending,InProgress,Completed,Cancelled") + .Annotation("Npgsql:Enum:reservation_status", "Pending,Approved,Rejected,Active,Returned,Cancelled"); + + migrationBuilder.CreateTable( + name: "audit_logs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + EntityType = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + EntityId = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "audit_action", nullable: false), + OldValues = table.Column(type: "jsonb", nullable: true), + NewValues = table.Column(type: "jsonb", nullable: true), + PerformedById = table.Column(type: "uuid", nullable: true), + PerformedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + constraints: table => + { + table.PrimaryKey("PK_audit_logs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "equipments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + SerialNumber = table.Column(type: "text", nullable: false), + Status = table.Column(type: "asset_status", nullable: false), + Category = table.Column(type: "asset_category", nullable: false), + Description = table.Column(type: "text", nullable: false), + Price = table.Column(type: "numeric(18,2)", nullable: false), + Image = table.Column(type: "text", nullable: false), + ImageUrl = table.Column(type: "text", nullable: false), + ImageUrlHttps = table.Column(type: "text", nullable: false), + PurchaseDate = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), + SalvageValue = table.Column(type: "numeric(18,2)", nullable: false), + UsefulLifeYears = table.Column(type: "integer", nullable: false), + DepreciationMethod = table.Column(type: "depreciation_method", nullable: false), + WarrantyExpirationDate = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_equipments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "SaaSProviders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + WebsiteUrl = table.Column(type: "text", nullable: true), + Category = table.Column(type: "text", nullable: true), + ContactEmail = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SaaSProviders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "software_licenses", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + LicenseKey = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + TotalSeats = table.Column(type: "integer", nullable: false), + ExpirationDate = table.Column(type: "timestamp with time zone", nullable: true), + Price = table.Column(type: "numeric(18,2)", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_software_licenses", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "maintenance_requests", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + EquipmentId = table.Column(type: "uuid", nullable: false), + RequestedById = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "maintenance_status", nullable: false), + Priority = table.Column(type: "maintenance_priority", nullable: false), + Description = table.Column(type: "text", nullable: false), + ScheduledDate = table.Column(type: "timestamp with time zone", nullable: true), + CompletedDate = table.Column(type: "timestamp with time zone", nullable: true), + TechnicianNotes = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + constraints: table => + { + table.PrimaryKey("PK_maintenance_requests", x => x.Id); + table.ForeignKey( + name: "FK_maintenance_requests_equipments_EquipmentId", + column: x => x.EquipmentId, + principalTable: "equipments", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "reservations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + EquipmentId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "reservation_status", nullable: false), + StartDate = table.Column(type: "timestamp with time zone", nullable: false), + EndDate = table.Column(type: "timestamp with time zone", nullable: true), + ReturnDate = table.Column(type: "timestamp with time zone", nullable: true), + Notes = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), + ApproverId = table.Column(type: "uuid", nullable: true), + ApprovedAt = table.Column(type: "timestamp with time zone", nullable: true), + RejectionReason = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_reservations", x => x.Id); + table.ForeignKey( + name: "FK_reservations_equipments_EquipmentId", + column: x => x.EquipmentId, + principalTable: "equipments", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "SaaSSubscriptions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProviderId = table.Column(type: "uuid", nullable: false), + PlanName = table.Column(type: "text", nullable: false), + BillingCycle = table.Column(type: "text", nullable: false), + CostPerSeat = table.Column(type: "numeric", nullable: false), + TotalSeats = table.Column(type: "integer", nullable: false), + RenewalDate = table.Column(type: "timestamp with time zone", nullable: true), + Status = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SaaSSubscriptions", x => x.Id); + table.ForeignKey( + name: "FK_SaaSSubscriptions_SaaSProviders_ProviderId", + column: x => x.ProviderId, + principalTable: "SaaSProviders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "equipment_licenses", + columns: table => new + { + EquipmentId = table.Column(type: "uuid", nullable: false), + SoftwareLicenseId = table.Column(type: "uuid", nullable: false), + AssignedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_equipment_licenses", x => new { x.EquipmentId, x.SoftwareLicenseId }); + table.ForeignKey( + name: "FK_equipment_licenses_equipments_EquipmentId", + column: x => x.EquipmentId, + principalTable: "equipments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_equipment_licenses_software_licenses_SoftwareLicenseId", + column: x => x.SoftwareLicenseId, + principalTable: "software_licenses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SaaSAssignments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + SubscriptionId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + AssignedDate = table.Column(type: "timestamp with time zone", nullable: false), + LastLoginDate = table.Column(type: "timestamp with time zone", nullable: true), + IsActive = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SaaSAssignments", x => x.Id); + table.ForeignKey( + name: "FK_SaaSAssignments_SaaSSubscriptions_SubscriptionId", + column: x => x.SubscriptionId, + principalTable: "SaaSSubscriptions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_equipment_licenses_SoftwareLicenseId", + table: "equipment_licenses", + column: "SoftwareLicenseId"); + + migrationBuilder.CreateIndex( + name: "IX_maintenance_requests_EquipmentId", + table: "maintenance_requests", + column: "EquipmentId"); + + migrationBuilder.CreateIndex( + name: "IX_reservations_EquipmentId", + table: "reservations", + column: "EquipmentId"); + + migrationBuilder.CreateIndex( + name: "IX_SaaSAssignments_SubscriptionId", + table: "SaaSAssignments", + column: "SubscriptionId"); + + migrationBuilder.CreateIndex( + name: "IX_SaaSSubscriptions_ProviderId", + table: "SaaSSubscriptions", + column: "ProviderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "audit_logs"); + + migrationBuilder.DropTable( + name: "equipment_licenses"); + + migrationBuilder.DropTable( + name: "maintenance_requests"); + + migrationBuilder.DropTable( + name: "reservations"); + + migrationBuilder.DropTable( + name: "SaaSAssignments"); + + migrationBuilder.DropTable( + name: "software_licenses"); + + migrationBuilder.DropTable( + name: "equipments"); + + migrationBuilder.DropTable( + name: "SaaSSubscriptions"); + + migrationBuilder.DropTable( + name: "SaaSProviders"); + } + } +} diff --git a/TradeOffStackAPI/Migrations/AssetDbContextModelSnapshot.cs b/TradeOffStackAPI/Migrations/AssetDbContextModelSnapshot.cs index ec3d3bb..a081a5e 100644 --- a/TradeOffStackAPI/Migrations/AssetDbContextModelSnapshot.cs +++ b/TradeOffStackAPI/Migrations/AssetDbContextModelSnapshot.cs @@ -21,6 +21,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasAnnotation("ProductVersion", "8.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 63); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "asset_category", new[] { "None", "Laptop", "Monitor", "Peripheral", "NetworkDevice" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "asset_status", new[] { "None", "Available", "Reserved", "OutForRepair", "Retired" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "audit_action", new[] { "Created", "Updated", "Deleted" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "depreciation_method", new[] { "None", "StraightLine", "DecliningBalance" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "maintenance_priority", new[] { "Low", "Medium", "High", "Critical" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "maintenance_status", new[] { "Pending", "InProgress", "Completed", "Cancelled" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "reservation_status", new[] { "Pending", "Approved", "Rejected", "Active", "Returned", "Cancelled" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("TradeOffStackAPI.Models.AuditLog", b => @@ -68,36 +75,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("audit_logs", (string)null); }); - modelBuilder.Entity("TradeOffStackAPI.Models.Department", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasAnnotation("Relational:JsonPropertyName", "id"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasDefaultValueSql("CURRENT_TIMESTAMP") - .HasAnnotation("Relational:JsonPropertyName", "created_at"); - - b.Property("Description") - .HasColumnType("text") - .HasAnnotation("Relational:JsonPropertyName", "description"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)") - .HasAnnotation("Relational:JsonPropertyName", "name"); - - b.HasKey("Id"); - - b.ToTable("departments", (string)null); - - b.HasAnnotation("Relational:JsonPropertyName", "department"); - }); - modelBuilder.Entity("TradeOffStackAPI.Models.Equipment", b => { b.Property("Id") @@ -254,15 +231,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasAnnotation("Relational:JsonPropertyName", "technician_notes"); - b.Property("UserId") - .HasColumnType("uuid"); - b.HasKey("Id"); b.HasIndex("EquipmentId"); - b.HasIndex("UserId"); - b.ToTable("maintenance_requests", (string)null); }); @@ -324,8 +296,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("EquipmentId"); - b.HasIndex("UserId"); - b.ToTable("reservations", (string)null); }); @@ -487,80 +457,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasAnnotation("Relational:JsonPropertyName", "software_license"); }); - modelBuilder.Entity("TradeOffStackAPI.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasAnnotation("Relational:JsonPropertyName", "id"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasDefaultValueSql("CURRENT_TIMESTAMP") - .HasAnnotation("Relational:JsonPropertyName", "created_at"); - - b.Property("DepartmentId") - .HasColumnType("uuid") - .HasAnnotation("Relational:JsonPropertyName", "department_id"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)") - .HasAnnotation("Relational:JsonPropertyName", "email"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasAnnotation("Relational:JsonPropertyName", "first_name"); - - b.Property("IsActive") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true) - .HasAnnotation("Relational:JsonPropertyName", "is_active"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasAnnotation("Relational:JsonPropertyName", "last_name"); - - b.Property("PasswordHash") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("PhoneNumber") - .HasMaxLength(20) - .HasColumnType("character varying(20)") - .HasAnnotation("Relational:JsonPropertyName", "phone_number"); - - b.Property("ProfileImage") - .HasColumnType("text") - .HasAnnotation("Relational:JsonPropertyName", "profile_image"); - - b.Property("ProfileImageUrl") - .HasColumnType("text") - .HasAnnotation("Relational:JsonPropertyName", "profile_image_url"); - - b.Property("Role") - .HasColumnType("integer") - .HasColumnName("Role") - .HasAnnotation("Relational:JsonPropertyName", "role"); - - b.HasKey("Id"); - - b.HasIndex("DepartmentId"); - - b.HasIndex("Email") - .IsUnique(); - - b.ToTable("users", (string)null); - }); - modelBuilder.Entity("TradeOffStackAPI.Models.EquipmentLicense", b => { b.HasOne("TradeOffStackAPI.Models.Equipment", "Equipment") @@ -588,10 +484,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("TradeOffStackAPI.Models.User", null) - .WithMany("MaintenanceRequests") - .HasForeignKey("UserId"); - b.Navigation("Equipment"); }); @@ -603,12 +495,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("TradeOffStackAPI.Models.User", null) - .WithMany("Reservations") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - b.Navigation("Equipment"); }); @@ -634,21 +520,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Provider"); }); - modelBuilder.Entity("TradeOffStackAPI.Models.User", b => - { - b.HasOne("TradeOffStackAPI.Models.Department", "Department") - .WithMany("Users") - .HasForeignKey("DepartmentId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("Department"); - }); - - modelBuilder.Entity("TradeOffStackAPI.Models.Department", b => - { - b.Navigation("Users"); - }); - modelBuilder.Entity("TradeOffStackAPI.Models.Equipment", b => { b.Navigation("EquipmentLicenses"); @@ -672,13 +543,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("EquipmentLicenses"); }); - - modelBuilder.Entity("TradeOffStackAPI.Models.User", b => - { - b.Navigation("MaintenanceRequests"); - - b.Navigation("Reservations"); - }); #pragma warning restore 612, 618 } } diff --git a/TradeOffStackAPI/Migrations/Core/20260612101300_InitialCore.Designer.cs b/TradeOffStackAPI/Migrations/Core/20260612101300_InitialCore.Designer.cs new file mode 100644 index 0000000..5a7877d --- /dev/null +++ b/TradeOffStackAPI/Migrations/Core/20260612101300_InitialCore.Designer.cs @@ -0,0 +1,197 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TradeOffStackAPI.Data; +using TradeOffStackAPI.Models; + +#nullable disable + +namespace TradeOffStackAPI.Migrations.Core +{ + [DbContext(typeof(CoreDbContext))] + [Migration("20260612101300_InitialCore")] + partial class InitialCore + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "audit_action", new[] { "Created", "Updated", "Deleted" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "user_role", new[] { "Admin", "Manager", "Employee", "Tester" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TradeOffStackAPI.Models.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "id"); + + b.Property("Action") + .HasColumnType("audit_action") + .HasColumnName("Action") + .HasAnnotation("Relational:JsonPropertyName", "action"); + + b.Property("EntityId") + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "entity_id"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasAnnotation("Relational:JsonPropertyName", "entity_type"); + + b.Property("NewValues") + .HasColumnType("jsonb") + .HasAnnotation("Relational:JsonPropertyName", "new_values"); + + b.Property("OldValues") + .HasColumnType("jsonb") + .HasAnnotation("Relational:JsonPropertyName", "old_values"); + + b.Property("PerformedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP") + .HasAnnotation("Relational:JsonPropertyName", "performed_at"); + + b.Property("PerformedById") + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "performed_by_id"); + + b.HasKey("Id"); + + b.ToTable("audit_logs", (string)null); + }); + + modelBuilder.Entity("TradeOffStackAPI.Models.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP") + .HasAnnotation("Relational:JsonPropertyName", "created_at"); + + b.Property("Description") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasAnnotation("Relational:JsonPropertyName", "name"); + + b.HasKey("Id"); + + b.ToTable("departments", (string)null); + + b.HasAnnotation("Relational:JsonPropertyName", "department"); + }); + + modelBuilder.Entity("TradeOffStackAPI.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP") + .HasAnnotation("Relational:JsonPropertyName", "created_at"); + + b.Property("DepartmentId") + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "department_id"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasAnnotation("Relational:JsonPropertyName", "email"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasAnnotation("Relational:JsonPropertyName", "first_name"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasAnnotation("Relational:JsonPropertyName", "is_active"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasAnnotation("Relational:JsonPropertyName", "last_name"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasAnnotation("Relational:JsonPropertyName", "phone_number"); + + b.Property("ProfileImage") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "profile_image"); + + b.Property("ProfileImageUrl") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "profile_image_url"); + + b.Property("Role") + .HasColumnType("user_role") + .HasColumnName("Role") + .HasAnnotation("Relational:JsonPropertyName", "role"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TradeOffStackAPI.Models.User", b => + { + b.HasOne("TradeOffStackAPI.Models.Department", "Department") + .WithMany("Users") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("TradeOffStackAPI.Models.Department", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TradeOffStackAPI/Migrations/Core/20260612101300_InitialCore.cs b/TradeOffStackAPI/Migrations/Core/20260612101300_InitialCore.cs new file mode 100644 index 0000000..481be2a --- /dev/null +++ b/TradeOffStackAPI/Migrations/Core/20260612101300_InitialCore.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using TradeOffStackAPI.Models; + +#nullable disable + +namespace TradeOffStackAPI.Migrations.Core +{ + /// + public partial class InitialCore : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:audit_action", "Created,Updated,Deleted") + .Annotation("Npgsql:Enum:user_role", "Admin,Manager,Employee,Tester"); + + migrationBuilder.CreateTable( + name: "audit_logs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + EntityType = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + EntityId = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "audit_action", nullable: false), + OldValues = table.Column(type: "jsonb", nullable: true), + NewValues = table.Column(type: "jsonb", nullable: true), + PerformedById = table.Column(type: "uuid", nullable: true), + PerformedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + constraints: table => + { + table.PrimaryKey("PK_audit_logs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "departments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Description = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + constraints: table => + { + table.PrimaryKey("PK_departments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FirstName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + LastName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Email = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + PhoneNumber = table.Column(type: "character varying(20)", maxLength: 20, nullable: true), + ProfileImage = table.Column(type: "text", nullable: true), + ProfileImageUrl = table.Column(type: "text", nullable: true), + Role = table.Column(type: "user_role", nullable: false), + PasswordHash = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + DepartmentId = table.Column(type: "uuid", nullable: true), + IsActive = table.Column(type: "boolean", nullable: false, defaultValue: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + constraints: table => + { + table.PrimaryKey("PK_users", x => x.Id); + table.ForeignKey( + name: "FK_users_departments_DepartmentId", + column: x => x.DepartmentId, + principalTable: "departments", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_users_DepartmentId", + table: "users", + column: "DepartmentId"); + + migrationBuilder.CreateIndex( + name: "IX_users_Email", + table: "users", + column: "Email", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "audit_logs"); + + migrationBuilder.DropTable( + name: "users"); + + migrationBuilder.DropTable( + name: "departments"); + } + } +} diff --git a/TradeOffStackAPI/Migrations/Core/CoreDbContextModelSnapshot.cs b/TradeOffStackAPI/Migrations/Core/CoreDbContextModelSnapshot.cs new file mode 100644 index 0000000..5486fc4 --- /dev/null +++ b/TradeOffStackAPI/Migrations/Core/CoreDbContextModelSnapshot.cs @@ -0,0 +1,194 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TradeOffStackAPI.Data; +using TradeOffStackAPI.Models; + +#nullable disable + +namespace TradeOffStackAPI.Migrations.Core +{ + [DbContext(typeof(CoreDbContext))] + partial class CoreDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "audit_action", new[] { "Created", "Updated", "Deleted" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "user_role", new[] { "Admin", "Manager", "Employee", "Tester" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TradeOffStackAPI.Models.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "id"); + + b.Property("Action") + .HasColumnType("audit_action") + .HasColumnName("Action") + .HasAnnotation("Relational:JsonPropertyName", "action"); + + b.Property("EntityId") + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "entity_id"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasAnnotation("Relational:JsonPropertyName", "entity_type"); + + b.Property("NewValues") + .HasColumnType("jsonb") + .HasAnnotation("Relational:JsonPropertyName", "new_values"); + + b.Property("OldValues") + .HasColumnType("jsonb") + .HasAnnotation("Relational:JsonPropertyName", "old_values"); + + b.Property("PerformedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP") + .HasAnnotation("Relational:JsonPropertyName", "performed_at"); + + b.Property("PerformedById") + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "performed_by_id"); + + b.HasKey("Id"); + + b.ToTable("audit_logs", (string)null); + }); + + modelBuilder.Entity("TradeOffStackAPI.Models.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP") + .HasAnnotation("Relational:JsonPropertyName", "created_at"); + + b.Property("Description") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasAnnotation("Relational:JsonPropertyName", "name"); + + b.HasKey("Id"); + + b.ToTable("departments", (string)null); + + b.HasAnnotation("Relational:JsonPropertyName", "department"); + }); + + modelBuilder.Entity("TradeOffStackAPI.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP") + .HasAnnotation("Relational:JsonPropertyName", "created_at"); + + b.Property("DepartmentId") + .HasColumnType("uuid") + .HasAnnotation("Relational:JsonPropertyName", "department_id"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasAnnotation("Relational:JsonPropertyName", "email"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasAnnotation("Relational:JsonPropertyName", "first_name"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasAnnotation("Relational:JsonPropertyName", "is_active"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasAnnotation("Relational:JsonPropertyName", "last_name"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasAnnotation("Relational:JsonPropertyName", "phone_number"); + + b.Property("ProfileImage") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "profile_image"); + + b.Property("ProfileImageUrl") + .HasColumnType("text") + .HasAnnotation("Relational:JsonPropertyName", "profile_image_url"); + + b.Property("Role") + .HasColumnType("user_role") + .HasColumnName("Role") + .HasAnnotation("Relational:JsonPropertyName", "role"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TradeOffStackAPI.Models.User", b => + { + b.HasOne("TradeOffStackAPI.Models.Department", "Department") + .WithMany("Users") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("TradeOffStackAPI.Models.Department", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/frontend/public/cisco_router.jpg b/frontend/public/cisco_router.jpg new file mode 100644 index 0000000..1b836b5 --- /dev/null +++ b/frontend/public/cisco_router.jpg @@ -0,0 +1 @@ +File not found: /v1/AUTH_mw/wikipedia-commons-local-public.4b/4/4b/Cisco_1841_router.jpg \ No newline at end of file diff --git a/frontend/src/utils/badgeUtils.tsx b/frontend/src/utils/badgeUtils.tsx index d07a5cc..c5f4c22 100644 --- a/frontend/src/utils/badgeUtils.tsx +++ b/frontend/src/utils/badgeUtils.tsx @@ -1,6 +1,6 @@ import { Badge } from '@/components/ui/badge'; -export const getRoleBadge = (role: string, _isFr: boolean) => { +export const getRoleBadge = (role: string) => { const styles: Record = { Admin: 'bg-rose-500/10 text-rose-500 border-rose-500/20', Manager: 'bg-amber-500/10 text-amber-500 border-amber-500/20',