From e35de1614f01d4347ec17fd8edf312e664b7cb1c Mon Sep 17 00:00:00 2001 From: Alessio Dal Santo Date: Fri, 23 Jan 2026 15:52:15 +0100 Subject: [PATCH] [Feature] Disabilitata deletion sync nei trasferimenti manuali e aggiunta configurazione nelle schedulazioni MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Disabilitata completamente la sincronizzazione eliminazioni nei trasferimenti manuali (DataCoupler.razor.cs) - Aggiunto campo EnableDeletionSync al modello ProfileSchedule (default: false) - Implementata logica condizionale in ScheduledProfileExecutionService per deletion sync - Aggiunta sezione 'Opzioni Avanzate' nell'interfaccia schedulazione con warning - Creata migration Entity Framework AddEnableDeletionSyncToProfileSchedule - Aggiornato BackupModels per supporto backup/restore del nuovo campo - Aggiornata documentazione README.md e copilot-instructions.md - La deletion sync è ora disponibile solo per schedulazioni con configurazione esplicita per massima sicurezza --- .github/copilot-instructions.md | 15 + ...eDeletionSyncToProfileSchedule.Designer.cs | 585 ++++++++++++++++++ ..._AddEnableDeletionSyncToProfileSchedule.cs | 29 + .../CredentialDbContextModelSnapshot.cs | 3 + CredentialManager/Models/ProfileSchedule.cs | 3 + CredentialManager/design_time_temp.db | Bin 155648 -> 155648 bytes Data_Coupler/Models/BackupModels.cs | 3 + Data_Coupler/Pages/DataCoupler.razor.cs | 5 +- Data_Coupler/Pages/Scheduling.razor | 24 + .../IScheduledProfileExecutionService.cs | 7 + .../ScheduledExecutionBackgroundService.cs | 8 +- .../ScheduledProfileExecutionService.cs | 126 +++- README.md | 4 + 13 files changed, 796 insertions(+), 16 deletions(-) create mode 100644 CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.Designer.cs create mode 100644 CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.cs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4a46459..e0b2f96 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -145,6 +145,7 @@ - **Storico Esecuzioni**: Log completo con timestamp, record processati, errori - **Pausa/Riprendi**: Controllo dinamico schedulazioni - **Override Database**: Possibilità di sovrascrivere sorgente/destinazione +- **Deletion Sync Configurabile**: Opzione per abilitare sincronizzazione eliminazioni (disabilitata di default) #### File Chiave: - `CredentialManager/Models/ProfileSchedule.cs` @@ -191,9 +192,23 @@ - **Gestione Associazioni**: Aggiorna/elimina associazioni correlate - **Modalità Sicura**: Preview eliminazioni prima dell'esecuzione - **Logging Completo**: Traccia tutte le operazioni di eliminazione +- **Configurazione Granulare**: + - **Disabilitata** completamente nei trasferimenti manuali (DataCoupler.razor) + - **Configurabile** nelle schedulazioni tramite flag `EnableDeletionSync` + - **Default: false** per massima sicurezza + - Warning esplicito nell'UI per operazioni critiche + +#### Sicurezza: +- La funzionalità è **disabilitata di default** per evitare eliminazioni accidentali +- Disponibile **solo per le schedulazioni** con configurazione esplicita +- L'utente deve attivamente abilitare la funzione con piena consapevolezza +- Logging completo di tutte le operazioni di eliminazione per audit trail #### File Chiave: - `Data_Coupler/Services/DeletionSyncService.cs` +- `Data_Coupler/Services/ScheduledProfileExecutionService.cs` +- `CredentialManager/Models/ProfileSchedule.cs` (campo `EnableDeletionSync`) +- `Data_Coupler/Pages/Scheduling.razor` (UI configurazione) ### 8. Sistema di Autenticazione diff --git a/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.Designer.cs b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.Designer.cs new file mode 100644 index 0000000..897a8d6 --- /dev/null +++ b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.Designer.cs @@ -0,0 +1,585 @@ +// +using System; +using CredentialManager.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CredentialManager.Migrations +{ + [DbContext(typeof(CredentialDbContext))] + [Migration("20260123104841_AddEnableDeletionSyncToProfileSchedule")] + partial class AddEnableDeletionSyncToProfileSchedule + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CommandTimeout") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(30); + + b.Property("ConnectionString") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("EncryptedApiKey") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedAuthToken") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedPassword") + .HasColumnType("TEXT"); + + b.Property("Headers") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IgnoreSslErrors") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("RestServiceType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TimeoutSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(100); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DatabaseType"); + + b.HasIndex("IsActive"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Type"); + + b.ToTable("Credentials", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DeletionAction") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("DeletionMarkField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeletionMarkValue") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationCredentialId") + .HasColumnType("INTEGER"); + + b.Property("DestinationEndpoint") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationSchema") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationTable") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FieldMappingJson") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceCredentialId") + .HasColumnType("INTEGER"); + + b.Property("SourceCustomQuery") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SourceDatabaseName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceFilePath") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceSchema") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceTable") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SyncDeletions") + .HasColumnType("INTEGER"); + + b.Property("UseRecordAssociations") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DestinationCredentialId"); + + b.HasIndex("DestinationType"); + + b.HasIndex("IsActive"); + + b.HasIndex("LastUsedAt"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("SourceCredentialId"); + + b.HasIndex("SourceType"); + + b.ToTable("DataCouplerProfiles", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Data_Hash") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DeletionSynced") + .HasColumnType("INTEGER"); + + b.Property("DeletionSyncedAt") + .HasColumnType("TEXT"); + + b.Property("DestinationEntity") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IsSourceDeleted") + .HasColumnType("INTEGER"); + + b.Property("KeyValue") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastVerifiedAt") + .HasColumnType("TEXT"); + + b.Property("MappedDestinationField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RestCredentialName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourcesInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DestinationEntity"); + + b.HasIndex("IsActive"); + + b.HasIndex("KeyValue") + .HasDatabaseName("IX_KeyAssociations_KeyValue"); + + b.HasIndex("LastVerifiedAt"); + + b.HasIndex("RestCredentialName"); + + b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName") + .IsUnique() + .HasDatabaseName("IX_KeyAssociations_Unique"); + + b.ToTable("KeyAssociations", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DailyTime") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DayOfMonth") + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationDatabaseOverride") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("EnableDeletionSync") + .HasColumnType("INTEGER"); + + b.Property("ExecutionCount") + .HasColumnType("INTEGER"); + + b.Property("IntervalUnit") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("IntervalValue") + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastExecutionMessage") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastExecutionRecordCount") + .HasColumnType("INTEGER"); + + b.Property("LastExecutionStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastExecutionTime") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("NextExecutionTime") + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("INTEGER"); + + b.Property("ScheduleType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ScheduledDateTime") + .HasColumnType("TEXT"); + + b.Property("SourceDatabaseOverride") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("ProfileSchedules"); + }); + + modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DestinationInfo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("Message") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("INTEGER"); + + b.Property("ProfileName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RecordsProcessed") + .HasColumnType("INTEGER"); + + b.Property("RecordsWithErrors") + .HasColumnType("INTEGER"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SourceInfo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SourceType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("TriggeredBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ScheduleId"); + + b.HasIndex("StartTime"); + + b.HasIndex("Status"); + + b.HasIndex("TriggerType"); + + b.ToTable("ScheduleExecutionHistories", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b => + { + b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential") + .WithMany() + .HasForeignKey("DestinationCredentialId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential") + .WithMany() + .HasForeignKey("SourceCredentialId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DestinationCredential"); + + b.Navigation("SourceCredential"); + }); + + modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b => + { + b.HasOne("CredentialManager.Models.DataCouplerProfile", "Profile") + .WithMany() + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b => + { + b.HasOne("CredentialManager.Models.ProfileSchedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Schedule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.cs b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.cs new file mode 100644 index 0000000..d202b37 --- /dev/null +++ b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CredentialManager.Migrations +{ + /// + public partial class AddEnableDeletionSyncToProfileSchedule : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EnableDeletionSync", + table: "ProfileSchedules", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EnableDeletionSync", + table: "ProfileSchedules"); + } + } +} diff --git a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs index 885c024..64a742e 100644 --- a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs +++ b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs @@ -382,6 +382,9 @@ namespace CredentialManager.Migrations .HasMaxLength(100) .HasColumnType("TEXT"); + b.Property("EnableDeletionSync") + .HasColumnType("INTEGER"); + b.Property("ExecutionCount") .HasColumnType("INTEGER"); diff --git a/CredentialManager/Models/ProfileSchedule.cs b/CredentialManager/Models/ProfileSchedule.cs index 9de9e3c..593931f 100644 --- a/CredentialManager/Models/ProfileSchedule.cs +++ b/CredentialManager/Models/ProfileSchedule.cs @@ -70,6 +70,9 @@ public class ProfileSchedule [MaxLength(100)] public string? DestinationDatabaseOverride { get; set; } + // Configurazione sincronizzazione eliminazioni (default: disabilitata) + public bool EnableDeletionSync { get; set; } = false; + // Metadati [MaxLength(100)] public string? CreatedBy { get; set; } diff --git a/CredentialManager/design_time_temp.db b/CredentialManager/design_time_temp.db index 589aa35dca8550323b36f665b93becefa8b7e81e..0482876d9dc3d3ece5fbe504db118508dfc3599c 100644 GIT binary patch delta 337 zcmZoTz}awsbAq%WD+2?A8W6*P$3z`tM%Il9P4W(Ax(0^2M#c(;2395(R;I>!mc}OL zW|l_U1_o9J2E0HWyu5)7{JZ$O`2+Z6_+IiI;G4pi$!E&P$h)6+0&n1EK>>YUZChUE za&aRABQpa-BV$7Y6AKf=c*m3!*Sy4}oK%<8oYa!c{Jh}GyyTGl$qjlUnMl^9S(D@V(?az&C|2lh2fok#|4ugw28i1-zRl=$0_a z@bI2x;6K1WjX#0kke`|FKHm<$NqotCCcNKy&jOXLyST0{ diff --git a/Data_Coupler/Pages/DataCoupler.razor.cs b/Data_Coupler/Pages/DataCoupler.razor.cs index 6cf9b76..ff532e7 100644 --- a/Data_Coupler/Pages/DataCoupler.razor.cs +++ b/Data_Coupler/Pages/DataCoupler.razor.cs @@ -1496,8 +1496,10 @@ public partial class DataCoupler : ComponentBase recordNumber++; } - // 3.5 Sincronizza le cancellazioni (se abilitato) + // 3.5 Sincronizzazione cancellazioni (DISABILITATA per trasferimenti manuali) + // Questa funzionalità è disponibile solo per le schedulazioni con configurazione esplicita int deletedCount = 0; + /* DELETION SYNC DISABILITATA PER TRASFERIMENTI MANUALI if (useRecordAssociations && !string.IsNullOrEmpty(sourceKeyField)) { try @@ -1570,6 +1572,7 @@ public partial class DataCoupler : ComponentBase }); } } + */ // 4. Mostra risultati if (errorCount == 0) diff --git a/Data_Coupler/Pages/Scheduling.razor b/Data_Coupler/Pages/Scheduling.razor index d780b72..d6bd0b3 100644 --- a/Data_Coupler/Pages/Scheduling.razor +++ b/Data_Coupler/Pages/Scheduling.razor @@ -336,6 +336,30 @@ +
+
+
+ Opzioni Avanzate +
+
+
+
+ + +
+
+ + + Attenzione: Se abilitata, i record eliminati dalla sorgente saranno automaticamente eliminati anche dalla destinazione durante l'esecuzione schedulata. + Questa opzione è disabilitata di default per motivi di sicurezza. + Usare con cautela! + +
+
+
+