diff --git a/Components/Components.csproj b/Components/Components.csproj new file mode 100644 index 0000000..2935b41 --- /dev/null +++ b/Components/Components.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/Components/ProfileManagement.razor b/Components/ProfileManagement.razor new file mode 100644 index 0000000..8371d0d --- /dev/null +++ b/Components/ProfileManagement.razor @@ -0,0 +1,180 @@ +@* Componente per la gestione completa dei profili *@ + + +@if (ShowModal) +{ + +} + + +@if (ShowDeleteConfirm && ProfileToDelete != null) +{ + +} diff --git a/Components/ProfileManagement.razor.cs b/Components/ProfileManagement.razor.cs new file mode 100644 index 0000000..6735ba7 --- /dev/null +++ b/Components/ProfileManagement.razor.cs @@ -0,0 +1,145 @@ +using Microsoft.AspNetCore.Components; +using CredentialManager.Models; + +namespace Components; + +public partial class ProfileManagement +{ + [Parameter] public bool ShowModal { get; set; } + [Parameter] public List? Profiles { get; set; } + [Parameter] public EventCallback OnCloseModal { get; set; } + [Parameter] public EventCallback OnProfileLoaded { get; set; } + [Parameter] public EventCallback OnProfileDeleted { get; set; } + [Parameter] public bool IsLoading { get; set; } + + private string SearchTerm { get; set; } = ""; + private string Message { get; set; } = ""; + private string MessageType { get; set; } = "info"; + private bool ShowDeleteConfirm { get; set; } = false; + private bool IsDeleting { get; set; } = false; + private DataCouplerProfile? ProfileToDelete { get; set; } + + private void FilterProfiles(ChangeEventArgs e) + { + SearchTerm = e.Value?.ToString() ?? ""; + } + + private IEnumerable GetFilteredProfiles() + { + if (Profiles == null) + return Enumerable.Empty(); + + if (string.IsNullOrWhiteSpace(SearchTerm)) + return Profiles; + + var searchLower = SearchTerm.ToLower(); + return Profiles.Where(p => + p.Name.ToLower().Contains(searchLower) || + (!string.IsNullOrEmpty(p.Description) && p.Description.ToLower().Contains(searchLower)) + ); + } + + private string GetTypeLabel(string type) + { + return type switch + { + "database" => "DB", + "file" => "File", + "rest" => "REST", + _ => type.ToUpper() + }; + } + + private string GetProfileSummary(DataCouplerProfile profile) + { + var parts = new List(); + + // Fonte + if (!string.IsNullOrEmpty(profile.SourceTable)) + parts.Add($"da {profile.SourceTable}"); + else if (!string.IsNullOrEmpty(profile.SourceFilePath)) + parts.Add($"da {Path.GetFileName(profile.SourceFilePath)}"); + + // Destinazione + if (!string.IsNullOrEmpty(profile.DestinationTable)) + parts.Add($"verso {profile.DestinationTable}"); + else if (!string.IsNullOrEmpty(profile.DestinationEndpoint)) + parts.Add($"verso {profile.DestinationEndpoint}"); + + return string.Join(" ", parts); + } + + private async Task CloseModal() + { + SearchTerm = ""; + Message = ""; + await OnCloseModal.InvokeAsync(); + } + + private async Task LoadProfile(DataCouplerProfile profile) + { + Message = $"Caricamento profilo '{profile.Name}'..."; + MessageType = "info"; + + await OnProfileLoaded.InvokeAsync(profile); + + Message = $"Profilo '{profile.Name}' caricato con successo!"; + MessageType = "success"; + + // Chiudi il modal dopo un breve delay + await Task.Delay(1000); + await CloseModal(); + } + + private void ConfirmDelete(DataCouplerProfile profile) + { + ProfileToDelete = profile; + ShowDeleteConfirm = true; + Message = ""; + } + + private void CancelDelete() + { + ProfileToDelete = null; + ShowDeleteConfirm = false; + } + + private async Task DeleteProfile() + { + if (ProfileToDelete == null) + return; + + IsDeleting = true; + + try + { + await OnProfileDeleted.InvokeAsync(ProfileToDelete.Id); + + Message = $"Profilo '{ProfileToDelete.Name}' eliminato con successo."; + MessageType = "success"; + + ShowDeleteConfirm = false; + ProfileToDelete = null; + } + catch (Exception ex) + { + Message = $"Errore nell'eliminazione: {ex.Message}"; + MessageType = "danger"; + } + finally + { + IsDeleting = false; + } + } + + public void SetMessage(string message, string type = "info") + { + Message = message; + MessageType = type; + } + + public void ClearMessage() + { + Message = ""; + } +} diff --git a/Components/ProfileQuickActions.razor b/Components/ProfileQuickActions.razor new file mode 100644 index 0000000..e69de29 diff --git a/Components/ProfileQuickActions.razor.cs b/Components/ProfileQuickActions.razor.cs new file mode 100644 index 0000000..e69de29 diff --git a/Components/ProfileSaver.razor b/Components/ProfileSaver.razor new file mode 100644 index 0000000..52142ad --- /dev/null +++ b/Components/ProfileSaver.razor @@ -0,0 +1,115 @@ +@* Componente per salvare la configurazione corrente come profilo *@ +
+
+
+ Salva Configurazione Corrente +
+
+
+ @if (!ShowSaveForm) + { + + @if (!CanSave) + { + + + Configura fonte e destinazione per abilitare il salvataggio + + } + } + else + { + + + +
+
+
+ + + +
+
+
+
+ + +
+
+
+ + @if (!string.IsNullOrEmpty(SaveMessage)) + { +
+ + @SaveMessage +
+ } + + +
+ +
+
+
+ Fonte: @GetSourceSummary()
+ @if (!string.IsNullOrEmpty(SourceSchema)) + { + Schema: @SourceSchema
+ } + @if (!string.IsNullOrEmpty(SourceTable)) + { + Tabella: @SourceTable + } +
+
+ Destinazione: @GetDestinationSummary()
+ @if (!string.IsNullOrEmpty(DestinationSchema)) + { + Schema: @DestinationSchema
+ } + @if (!string.IsNullOrEmpty(DestinationTable)) + { + Tabella: @DestinationTable + } + @if (!string.IsNullOrEmpty(DestinationEndpoint)) + { + Endpoint: @DestinationEndpoint + } +
+
+ @if (FieldMappings != null && FieldMappings.Any()) + { +
+ + + @FieldMappings.Count mapping dei campi configurati + + } +
+
+ +
+ + +
+
+ } +
+
diff --git a/Components/ProfileSaver.razor.cs b/Components/ProfileSaver.razor.cs new file mode 100644 index 0000000..a776486 --- /dev/null +++ b/Components/ProfileSaver.razor.cs @@ -0,0 +1,123 @@ +using Microsoft.AspNetCore.Components; +using CredentialManager.Models; +using System.ComponentModel.DataAnnotations; + +namespace Components; + +public partial class ProfileSaver +{ + [Parameter] public bool CanSave { get; set; } + [Parameter] public string SourceType { get; set; } = ""; + [Parameter] public int? SourceCredentialId { get; set; } + [Parameter] public string? SourceSchema { get; set; } + [Parameter] public string? SourceTable { get; set; } + [Parameter] public string? SourceFilePath { get; set; } + [Parameter] public string DestinationType { get; set; } = ""; + [Parameter] public int? DestinationCredentialId { get; set; } + [Parameter] public string? DestinationSchema { get; set; } + [Parameter] public string? DestinationTable { get; set; } + [Parameter] public string? DestinationEndpoint { get; set; } + [Parameter] public List? FieldMappings { get; set; } + [Parameter] public EventCallback OnProfileSaved { get; set; } + + private bool ShowSaveForm { get; set; } = false; + private bool IsSaving { get; set; } = false; + private string SaveMessage { get; set; } = ""; + private string SaveMessageType { get; set; } = "info"; + private ProfileFormModel ProfileData { get; set; } = new(); + + private void ShowSaveDialog() + { + ProfileData = new ProfileFormModel(); + ShowSaveForm = true; + SaveMessage = ""; + } + + private void CancelSave() + { + ShowSaveForm = false; + SaveMessage = ""; + ProfileData = new(); + } + + private async Task SaveProfile() + { + IsSaving = true; + SaveMessage = ""; + + try + { + var profileDto = new DataCouplerProfileDto + { + Name = ProfileData.Name, + Description = ProfileData.Description, + SourceType = SourceType, + SourceCredentialId = SourceCredentialId, + SourceSchema = SourceSchema, + SourceTable = SourceTable, + SourceFilePath = SourceFilePath, + DestinationType = DestinationType, + DestinationCredentialId = DestinationCredentialId, + DestinationSchema = DestinationSchema, + DestinationTable = DestinationTable, + DestinationEndpoint = DestinationEndpoint, + FieldMappings = FieldMappings + }; + + await OnProfileSaved.InvokeAsync(profileDto); + + SaveMessage = $"Profilo '{ProfileData.Name}' salvato con successo!"; + SaveMessageType = "success"; + + // Reset form after successful save + await Task.Delay(1500); // Show success message briefly + ShowSaveForm = false; + ProfileData = new(); + } + catch (Exception ex) + { + SaveMessage = $"Errore nel salvataggio: {ex.Message}"; + SaveMessageType = "danger"; + } + finally + { + IsSaving = false; + } + } + + private string GetSourceSummary() + { + return SourceType switch + { + "database" => "Database", + "file" => "File Excel/CSV", + _ => "Non configurato" + }; + } + + private string GetDestinationSummary() + { + return DestinationType switch + { + "database" => "Database", + "rest" => "REST API", + _ => "Non configurato" + }; + } + + public void SetMessage(string message, string type = "info") + { + SaveMessage = message; + SaveMessageType = type; + } + + public class ProfileFormModel + { + [Required(ErrorMessage = "Il nome del profilo è obbligatorio")] + [StringLength(100, ErrorMessage = "Il nome non può superare i 100 caratteri")] + public string Name { get; set; } = ""; + + [StringLength(500, ErrorMessage = "La descrizione non può superare i 500 caratteri")] + public string? Description { get; set; } + } +} diff --git a/Components/ProfileSelector.razor b/Components/ProfileSelector.razor new file mode 100644 index 0000000..28a3909 --- /dev/null +++ b/Components/ProfileSelector.razor @@ -0,0 +1,56 @@ +@* Componente per la selezione e caricamento dei profili *@ +
+
+
+ Gestione Profili +
+
+
+
+ +
+ +
+ + +
+ @if (!string.IsNullOrEmpty(LoadMessage)) + { +
+ + @LoadMessage +
+ } +
+ + +
+ +
+
+
+
diff --git a/Components/ProfileSelector.razor.cs b/Components/ProfileSelector.razor.cs new file mode 100644 index 0000000..66459fe --- /dev/null +++ b/Components/ProfileSelector.razor.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Components; +using CredentialManager.Models; + +namespace Components; + +public partial class ProfileSelector +{ + [Parameter] public List? Profiles { get; set; } + [Parameter] public EventCallback OnProfileLoaded { get; set; } + [Parameter] public EventCallback OnManageProfiles { get; set; } + [Parameter] public bool IsLoading { get; set; } + + private int SelectedProfileId { get; set; } + private string LoadMessage { get; set; } = ""; + private string LoadMessageType { get; set; } = "info"; + + private void OnProfileSelected(ChangeEventArgs e) + { + if (int.TryParse(e.Value?.ToString(), out int profileId)) + { + SelectedProfileId = profileId; + } + else + { + SelectedProfileId = 0; + } + LoadMessage = ""; + } + + private async Task LoadSelectedProfile() + { + if (SelectedProfileId == 0 || Profiles == null) + return; + + var selectedProfile = Profiles.FirstOrDefault(p => p.Id == SelectedProfileId); + if (selectedProfile != null) + { + LoadMessage = $"Profilo '{selectedProfile.Name}' caricato con successo!"; + LoadMessageType = "success"; + await OnProfileLoaded.InvokeAsync(selectedProfile); + } + else + { + LoadMessage = "Errore nel caricamento del profilo selezionato."; + LoadMessageType = "danger"; + } + } + + private async Task OpenProfileManagement() + { + await OnManageProfiles.InvokeAsync(); + } + + public void ClearMessage() + { + LoadMessage = ""; + } + + public void SetMessage(string message, string type = "info") + { + LoadMessage = message; + LoadMessageType = type; + } +} diff --git a/Components/_Imports.razor b/Components/_Imports.razor new file mode 100644 index 0000000..a3eeb12 --- /dev/null +++ b/Components/_Imports.razor @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using CredentialManager.Models +@using CredentialManager.Services diff --git a/Components/nuget.config b/Components/nuget.config new file mode 100644 index 0000000..6ce9759 --- /dev/null +++ b/Components/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Components/wwwroot/background.png b/Components/wwwroot/background.png new file mode 100644 index 0000000..e15a3bd Binary files /dev/null and b/Components/wwwroot/background.png differ diff --git a/CredentialManager/CredentialManagerConfiguration.cs b/CredentialManager/CredentialManagerConfiguration.cs index 0eaee03..8b1e849 100644 --- a/CredentialManager/CredentialManagerConfiguration.cs +++ b/CredentialManager/CredentialManagerConfiguration.cs @@ -40,6 +40,7 @@ public static class CredentialManagerConfiguration services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/CredentialManager/Data/CredentialDbContext.cs b/CredentialManager/Data/CredentialDbContext.cs index 5069d45..09a6c19 100644 --- a/CredentialManager/Data/CredentialDbContext.cs +++ b/CredentialManager/Data/CredentialDbContext.cs @@ -10,6 +10,7 @@ public class CredentialDbContext : DbContext { public DbSet Credentials { get; set; } public DbSet KeyAssociations { get; set; } + public DbSet DataCouplerProfiles { get; set; } public CredentialDbContext(DbContextOptions options) : base(options) { @@ -141,5 +142,80 @@ public class CredentialDbContext : DbContext entity.HasIndex(e => e.CreatedAt); entity.HasIndex(e => e.LastVerifiedAt); }); + + // Configurazione della tabella DataCouplerProfiles + modelBuilder.Entity(entity => + { + entity.ToTable("DataCouplerProfiles"); + + entity.HasKey(e => e.Id); + + entity.Property(e => e.Name) + .IsRequired() + .HasMaxLength(100); + + entity.Property(e => e.Description) + .HasMaxLength(500); + + entity.Property(e => e.SourceType) + .IsRequired() + .HasMaxLength(20); + + entity.Property(e => e.SourceSchema) + .HasMaxLength(200); + + entity.Property(e => e.SourceTable) + .HasMaxLength(200); + + entity.Property(e => e.SourceFilePath) + .HasMaxLength(500); + + entity.Property(e => e.DestinationType) + .IsRequired() + .HasMaxLength(20); + + entity.Property(e => e.DestinationSchema) + .HasMaxLength(200); + + entity.Property(e => e.DestinationTable) + .HasMaxLength(200); + + entity.Property(e => e.DestinationEndpoint) + .HasMaxLength(500); + + entity.Property(e => e.FieldMappingJson) + .HasMaxLength(4000); + + entity.Property(e => e.CreatedBy) + .HasMaxLength(100); + + // Valori di default + entity.Property(e => e.CreatedAt) + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + entity.Property(e => e.IsActive) + .HasDefaultValue(true); + + // Indici + entity.HasIndex(e => e.Name) + .IsUnique(); + + entity.HasIndex(e => e.SourceType); + entity.HasIndex(e => e.DestinationType); + entity.HasIndex(e => e.IsActive); + entity.HasIndex(e => e.CreatedAt); + entity.HasIndex(e => e.LastUsedAt); + + // Relazioni con le credenziali + entity.HasOne(e => e.SourceCredential) + .WithMany() + .HasForeignKey(e => e.SourceCredentialId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(e => e.DestinationCredential) + .WithMany() + .HasForeignKey(e => e.DestinationCredentialId) + .OnDelete(DeleteBehavior.SetNull); + }); } } diff --git a/CredentialManager/Data/CredentialDbContextFactory.cs b/CredentialManager/Data/CredentialDbContextFactory.cs new file mode 100644 index 0000000..2a7f3bd --- /dev/null +++ b/CredentialManager/Data/CredentialDbContextFactory.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace CredentialManager.Data; + +/// +/// Factory per creare il DbContext durante la fase di design (migrations) +/// +public class CredentialDbContextFactory : IDesignTimeDbContextFactory +{ + public CredentialDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + + // Usa un database SQLite temporaneo per le migrations + var connectionString = "Data Source=design_time_temp.db"; + optionsBuilder.UseSqlite(connectionString); + + return new CredentialDbContext(optionsBuilder.Options); + } +} diff --git a/CredentialManager/Migrations/20250701203438_AddDataCouplerProfiles.Designer.cs b/CredentialManager/Migrations/20250701203438_AddDataCouplerProfiles.Designer.cs new file mode 100644 index 0000000..0a8752e --- /dev/null +++ b/CredentialManager/Migrations/20250701203438_AddDataCouplerProfiles.Designer.cs @@ -0,0 +1,326 @@ +// +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("20250701203438_AddDataCouplerProfiles")] + partial class AddDataCouplerProfiles + { + /// + 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("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("SourceFilePath") + .HasMaxLength(500) + .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.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("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("KeyValue") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastVerifiedAt") + .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.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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CredentialManager/Migrations/20250701203438_AddDataCouplerProfiles.cs b/CredentialManager/Migrations/20250701203438_AddDataCouplerProfiles.cs new file mode 100644 index 0000000..068ac23 --- /dev/null +++ b/CredentialManager/Migrations/20250701203438_AddDataCouplerProfiles.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CredentialManager.Migrations +{ + /// + public partial class AddDataCouplerProfiles : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DataCouplerProfiles", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + SourceType = table.Column(type: "TEXT", maxLength: 20, nullable: false), + SourceCredentialId = table.Column(type: "INTEGER", nullable: true), + SourceSchema = table.Column(type: "TEXT", maxLength: 200, nullable: true), + SourceTable = table.Column(type: "TEXT", maxLength: 200, nullable: true), + SourceFilePath = table.Column(type: "TEXT", maxLength: 500, nullable: true), + DestinationType = table.Column(type: "TEXT", maxLength: 20, nullable: false), + DestinationCredentialId = table.Column(type: "INTEGER", nullable: true), + DestinationSchema = table.Column(type: "TEXT", maxLength: 200, nullable: true), + DestinationTable = table.Column(type: "TEXT", maxLength: 200, nullable: true), + DestinationEndpoint = table.Column(type: "TEXT", maxLength: 500, nullable: true), + FieldMappingJson = table.Column(type: "TEXT", maxLength: 4000, nullable: true), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), + LastUsedAt = table.Column(type: "TEXT", nullable: true), + IsActive = table.Column(type: "INTEGER", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DataCouplerProfiles", x => x.Id); + table.ForeignKey( + name: "FK_DataCouplerProfiles_Credentials_DestinationCredentialId", + column: x => x.DestinationCredentialId, + principalTable: "Credentials", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_DataCouplerProfiles_Credentials_SourceCredentialId", + column: x => x.SourceCredentialId, + principalTable: "Credentials", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_DataCouplerProfiles_CreatedAt", + table: "DataCouplerProfiles", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_DataCouplerProfiles_DestinationCredentialId", + table: "DataCouplerProfiles", + column: "DestinationCredentialId"); + + migrationBuilder.CreateIndex( + name: "IX_DataCouplerProfiles_DestinationType", + table: "DataCouplerProfiles", + column: "DestinationType"); + + migrationBuilder.CreateIndex( + name: "IX_DataCouplerProfiles_IsActive", + table: "DataCouplerProfiles", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_DataCouplerProfiles_LastUsedAt", + table: "DataCouplerProfiles", + column: "LastUsedAt"); + + migrationBuilder.CreateIndex( + name: "IX_DataCouplerProfiles_Name", + table: "DataCouplerProfiles", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DataCouplerProfiles_SourceCredentialId", + table: "DataCouplerProfiles", + column: "SourceCredentialId"); + + migrationBuilder.CreateIndex( + name: "IX_DataCouplerProfiles_SourceType", + table: "DataCouplerProfiles", + column: "SourceType"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DataCouplerProfiles"); + } + } +} diff --git a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs index 925feee..739f22a 100644 --- a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs +++ b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace CredentialManager.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b => { @@ -123,6 +123,104 @@ namespace CredentialManager.Migrations 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("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("SourceFilePath") + .HasMaxLength(500) + .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.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") @@ -202,6 +300,23 @@ namespace CredentialManager.Migrations b.ToTable("KeyAssociations", (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"); + }); #pragma warning restore 612, 618 } } diff --git a/CredentialManager/Models/DataCouplerProfile.cs b/CredentialManager/Models/DataCouplerProfile.cs new file mode 100644 index 0000000..047032a --- /dev/null +++ b/CredentialManager/Models/DataCouplerProfile.cs @@ -0,0 +1,73 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CredentialManager.Models; + +/// +/// Modello per salvare le configurazioni dei profili di Data Coupler +/// +public class DataCouplerProfile +{ + [Key] + public int Id { get; set; } + + [Required] + [MaxLength(100)] + public string Name { get; set; } = string.Empty; + + [MaxLength(500)] + public string? Description { get; set; } + + // Configurazione Fonte Dati + [Required] + [MaxLength(20)] + public string SourceType { get; set; } = string.Empty; // "database" o "file" + + public int? SourceCredentialId { get; set; } + + [MaxLength(200)] + public string? SourceSchema { get; set; } + + [MaxLength(200)] + public string? SourceTable { get; set; } + + [MaxLength(500)] + public string? SourceFilePath { get; set; } + + // Configurazione Destinazione + [Required] + [MaxLength(20)] + public string DestinationType { get; set; } = string.Empty; // "database" o "rest" + + public int? DestinationCredentialId { get; set; } + + [MaxLength(200)] + public string? DestinationSchema { get; set; } + + [MaxLength(200)] + public string? DestinationTable { get; set; } + + [MaxLength(500)] + public string? DestinationEndpoint { get; set; } + + // Mapping dei campi salvato come JSON + [MaxLength(4000)] + public string? FieldMappingJson { get; set; } + + // Metadati + [MaxLength(100)] + public string? CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public DateTime? LastUsedAt { get; set; } + + public bool IsActive { get; set; } = true; + + // Relazioni opzionali con le credenziali + [ForeignKey(nameof(SourceCredentialId))] + public virtual CredentialEntity? SourceCredential { get; set; } + + [ForeignKey(nameof(DestinationCredentialId))] + public virtual CredentialEntity? DestinationCredential { get; set; } +} diff --git a/CredentialManager/Models/DataCouplerProfileDto.cs b/CredentialManager/Models/DataCouplerProfileDto.cs new file mode 100644 index 0000000..86f7b2d --- /dev/null +++ b/CredentialManager/Models/DataCouplerProfileDto.cs @@ -0,0 +1,60 @@ +namespace CredentialManager.Models; + +/// +/// DTO per la creazione/aggiornamento di un profilo DataCoupler +/// +public class DataCouplerProfileDto +{ + public int? Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + + // Informazioni sorgente + public string SourceType { get; set; } = string.Empty; + public int? SourceCredentialId { get; set; } + public string? SourceSchema { get; set; } + public string? SourceTable { get; set; } + public string? SourceFilePath { get; set; } + + // Informazioni destinazione + public string DestinationType { get; set; } = string.Empty; + public int? DestinationCredentialId { get; set; } + public string? DestinationSchema { get; set; } + public string? DestinationTable { get; set; } + public string? DestinationEndpoint { get; set; } + + // Mapping dei campi + public List? FieldMappings { get; set; } +} + +/// +/// DTO per il mapping dei campi +/// +public class FieldMappingDto +{ + public string SourceField { get; set; } = string.Empty; + public string DestinationField { get; set; } = string.Empty; + public string? DataType { get; set; } + public bool IsKey { get; set; } + public bool IsRequired { get; set; } + public string? DefaultValue { get; set; } + public string? Transformation { get; set; } +} + +/// +/// DTO per la visualizzazione di un profilo nella lista +/// +public class DataCouplerProfileSummaryDto +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string SourceType { get; set; } = string.Empty; + public string? SourceName { get; set; } + public string DestinationType { get; set; } = string.Empty; + public string? DestinationName { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? LastUsedAt { get; set; } + public string? CreatedBy { get; set; } + public bool IsActive { get; set; } +} diff --git a/CredentialManager/Services/DataCouplerProfileService.cs b/CredentialManager/Services/DataCouplerProfileService.cs new file mode 100644 index 0000000..1dbc0d6 --- /dev/null +++ b/CredentialManager/Services/DataCouplerProfileService.cs @@ -0,0 +1,234 @@ +using Microsoft.EntityFrameworkCore; +using CredentialManager.Data; +using CredentialManager.Models; +using System.Text.Json; + +namespace CredentialManager.Services; + +/// +/// Implementazione del servizio per la gestione dei profili Data Coupler +/// +public class DataCouplerProfileService : IDataCouplerProfileService +{ + private readonly CredentialDbContext _context; + + public DataCouplerProfileService(CredentialDbContext context) + { + _context = context; + } + + /// + /// Ottiene tutti i profili attivi + /// + public async Task> GetAllProfilesAsync() + { + return await _context.DataCouplerProfiles + .Include(p => p.SourceCredential) + .Include(p => p.DestinationCredential) + .Where(p => p.IsActive) + .OrderByDescending(p => p.LastUsedAt) + .ThenByDescending(p => p.CreatedAt) + .ToListAsync(); + } + + /// + /// Ottiene un profilo per ID + /// + public async Task GetProfileByIdAsync(int id) + { + return await _context.DataCouplerProfiles + .Include(p => p.SourceCredential) + .Include(p => p.DestinationCredential) + .FirstOrDefaultAsync(p => p.Id == id && p.IsActive); + } + + /// + /// Ottiene un profilo per nome + /// + public async Task GetProfileByNameAsync(string name) + { + return await _context.DataCouplerProfiles + .Include(p => p.SourceCredential) + .Include(p => p.DestinationCredential) + .FirstOrDefaultAsync(p => p.Name == name && p.IsActive); + } + + /// + /// Salva un nuovo profilo + /// + public async Task SaveProfileAsync(DataCouplerProfile profile) + { + profile.CreatedAt = DateTime.UtcNow; + profile.IsActive = true; + + _context.DataCouplerProfiles.Add(profile); + await _context.SaveChangesAsync(); + + return profile; + } + + /// + /// Aggiorna un profilo esistente + /// + public async Task UpdateProfileAsync(DataCouplerProfile profile) + { + var existingProfile = await _context.DataCouplerProfiles + .FirstOrDefaultAsync(p => p.Id == profile.Id); + + if (existingProfile == null) + { + throw new InvalidOperationException($"Profilo con ID {profile.Id} non trovato"); + } + + // Aggiorna le proprietà + existingProfile.Name = profile.Name; + existingProfile.Description = profile.Description; + existingProfile.SourceType = profile.SourceType; + existingProfile.SourceCredentialId = profile.SourceCredentialId; + existingProfile.SourceSchema = profile.SourceSchema; + existingProfile.SourceTable = profile.SourceTable; + existingProfile.SourceFilePath = profile.SourceFilePath; + existingProfile.DestinationType = profile.DestinationType; + existingProfile.DestinationCredentialId = profile.DestinationCredentialId; + existingProfile.DestinationSchema = profile.DestinationSchema; + existingProfile.DestinationTable = profile.DestinationTable; + existingProfile.DestinationEndpoint = profile.DestinationEndpoint; + existingProfile.FieldMappingJson = profile.FieldMappingJson; + + await _context.SaveChangesAsync(); + return existingProfile; + } + + /// + /// Elimina un profilo (soft delete) + /// + public async Task DeleteProfileAsync(int id) + { + var profile = await _context.DataCouplerProfiles + .FirstOrDefaultAsync(p => p.Id == id); + + if (profile == null) + { + return false; + } + + profile.IsActive = false; + await _context.SaveChangesAsync(); + return true; + } + + /// + /// Aggiorna la data di ultimo utilizzo di un profilo + /// + public async Task UpdateLastUsedAsync(int id) + { + var profile = await _context.DataCouplerProfiles + .FirstOrDefaultAsync(p => p.Id == id); + + if (profile != null) + { + profile.LastUsedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + } + } + + /// + /// Verifica se esiste un profilo con il nome specificato + /// + public async Task ProfileExistsAsync(string name, int? excludeId = null) + { + var query = _context.DataCouplerProfiles + .Where(p => p.Name == name && p.IsActive); + + if (excludeId.HasValue) + { + query = query.Where(p => p.Id != excludeId.Value); + } + + return await query.AnyAsync(); + } + + /// + /// Serializza la lista di mapping dei campi in JSON + /// + public string SerializeFieldMappings(List? mappings) + { + if (mappings == null || !mappings.Any()) + return string.Empty; + + return JsonSerializer.Serialize(mappings, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + } + + /// + /// Deserializza il JSON dei mapping dei campi + /// + public List DeserializeFieldMappings(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return new List(); + + try + { + return JsonSerializer.Deserialize>(json, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }) ?? new List(); + } + catch + { + return new List(); + } + } + + /// + /// Converte un DataCouplerProfile in DTO + /// + public DataCouplerProfileDto ToDto(DataCouplerProfile profile) + { + return new DataCouplerProfileDto + { + Id = profile.Id, + Name = profile.Name, + Description = profile.Description, + SourceType = profile.SourceType, + SourceCredentialId = profile.SourceCredentialId, + SourceSchema = profile.SourceSchema, + SourceTable = profile.SourceTable, + SourceFilePath = profile.SourceFilePath, + DestinationType = profile.DestinationType, + DestinationCredentialId = profile.DestinationCredentialId, + DestinationSchema = profile.DestinationSchema, + DestinationTable = profile.DestinationTable, + DestinationEndpoint = profile.DestinationEndpoint, + FieldMappings = DeserializeFieldMappings(profile.FieldMappingJson) + }; + } + + /// + /// Converte un DTO in DataCouplerProfile + /// + public DataCouplerProfile FromDto(DataCouplerProfileDto dto, string? createdBy = null) + { + return new DataCouplerProfile + { + Id = dto.Id ?? 0, + Name = dto.Name, + Description = dto.Description, + SourceType = dto.SourceType, + SourceCredentialId = dto.SourceCredentialId, + SourceSchema = dto.SourceSchema, + SourceTable = dto.SourceTable, + SourceFilePath = dto.SourceFilePath, + DestinationType = dto.DestinationType, + DestinationCredentialId = dto.DestinationCredentialId, + DestinationSchema = dto.DestinationSchema, + DestinationTable = dto.DestinationTable, + DestinationEndpoint = dto.DestinationEndpoint, + FieldMappingJson = SerializeFieldMappings(dto.FieldMappings), + CreatedBy = createdBy + }; + } +} diff --git a/CredentialManager/Services/IDataCouplerProfileService.cs b/CredentialManager/Services/IDataCouplerProfileService.cs new file mode 100644 index 0000000..1a11cd2 --- /dev/null +++ b/CredentialManager/Services/IDataCouplerProfileService.cs @@ -0,0 +1,49 @@ +using CredentialManager.Models; + +namespace CredentialManager.Services; + +/// +/// Interfaccia per il servizio di gestione dei profili Data Coupler +/// +public interface IDataCouplerProfileService +{ + /// + /// Ottiene tutti i profili attivi + /// + Task> GetAllProfilesAsync(); + + /// + /// Ottiene un profilo per ID + /// + Task GetProfileByIdAsync(int id); + + /// + /// Ottiene un profilo per nome + /// + Task GetProfileByNameAsync(string name); + + /// + /// Salva un nuovo profilo + /// + Task SaveProfileAsync(DataCouplerProfile profile); + + /// + /// Aggiorna un profilo esistente + /// + Task UpdateProfileAsync(DataCouplerProfile profile); + + /// + /// Elimina un profilo + /// + Task DeleteProfileAsync(int id); + + /// + /// Aggiorna la data di ultimo utilizzo di un profilo + /// + Task UpdateLastUsedAsync(int id); + + /// + /// Verifica se esiste un profilo con il nome specificato + /// + Task ProfileExistsAsync(string name, int? excludeId = null); +} diff --git a/CredentialManager/design_time_temp.db b/CredentialManager/design_time_temp.db new file mode 100644 index 0000000..a1e3ded Binary files /dev/null and b/CredentialManager/design_time_temp.db differ diff --git a/Data_Coupler.sln b/Data_Coupler.sln index 03f047d..449531a 100644 --- a/Data_Coupler.sln +++ b/Data_Coupler.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataConnection", "DataConne EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialManager", "CredentialManager\CredentialManager.csproj", "{30B369DE-A0BA-4AD7-8895-7BEBD244E782}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components", "Components\Components.csproj", "{B5114CAC-3E03-4150-B93C-652882F66CB7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,6 +57,18 @@ Global {30B369DE-A0BA-4AD7-8895-7BEBD244E782}.Release|x64.Build.0 = Release|Any CPU {30B369DE-A0BA-4AD7-8895-7BEBD244E782}.Release|x86.ActiveCfg = Release|Any CPU {30B369DE-A0BA-4AD7-8895-7BEBD244E782}.Release|x86.Build.0 = Release|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|x64.Build.0 = Debug|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|x86.ActiveCfg = Debug|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|x86.Build.0 = Debug|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|Any CPU.Build.0 = Release|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x64.ActiveCfg = Release|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x64.Build.0 = Release|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x86.ActiveCfg = Release|Any CPU + {B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Data_Coupler/Data_Coupler.csproj b/Data_Coupler/Data_Coupler.csproj index ab66041..106e539 100644 --- a/Data_Coupler/Data_Coupler.csproj +++ b/Data_Coupler/Data_Coupler.csproj @@ -9,6 +9,7 @@ + diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor index c4d4298..d5c57fb 100644 --- a/Data_Coupler/Pages/DataCoupler.razor +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -15,6 +15,7 @@ @inject IDataConnectionFactory ConnectionFactory @inject IJSRuntime JSRuntime @inject ILogger Logger +@inject CredentialManager.Services.IDataCouplerProfileService ProfileService Data Coupler @@ -24,7 +25,19 @@

Data Coupler - Coupling Database e REST API

Connetti database e servizi REST per il trasferimento dati

-
+
+ + +
+
+ +
+
+ +
@@ -1103,6 +1116,31 @@ }
+ +@if (isDatabaseConnected && isRestConnected && fieldMappings.Any()) +{ +
+
+ +
+
+} + + + + @if (showDatabaseSelectionModal) { diff --git a/Data_Coupler/Pages/DataCoupler.razor.cs b/Data_Coupler/Pages/DataCoupler.razor.cs index baa9c24..880a2a5 100644 --- a/Data_Coupler/Pages/DataCoupler.razor.cs +++ b/Data_Coupler/Pages/DataCoupler.razor.cs @@ -2,6 +2,7 @@ using System; using System.Data; using System.Text; using CredentialManager.Models; +using CredentialManager.Services; using DataConnection.Interfaces; using DataConnection.REST.Interfaces; using DataConnection.REST.Models; @@ -109,6 +110,11 @@ public partial class DataCoupler private IDatabaseManager? currentDatabaseManager = null; private IRestMetadataDiscovery? currentRestDiscovery = null; private IRestServiceClient? currentRestClient = null; + + // Gestione Profili + private List availableProfiles = new(); + private bool isLoadingProfiles = false; + private bool showProfileManagement = false; protected override async Task OnInitializedAsync() { @@ -119,6 +125,8 @@ public partial class DataCoupler { databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync(); restApiCredentials = await CredentialService.GetAllRestApiCredentialsAsync(); + // Carica anche i profili disponibili + await LoadProfiles(); } catch (Exception ex) { @@ -127,6 +135,241 @@ public partial class DataCoupler } } + private async Task LoadProfiles() + { + try + { + isLoadingProfiles = true; + var profiles = await ProfileService.GetAllProfilesAsync(); + availableProfiles = profiles.ToList(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento dei profili"); + } + finally + { + isLoadingProfiles = false; + StateHasChanged(); + } + } + + private async Task OnProfileLoaded(DataCouplerProfile profile) + { + try + { + // Aggiorna la data di ultimo utilizzo + await ProfileService.UpdateLastUsedAsync(profile.Id); + + // Applica la configurazione del profilo + await ApplyProfileConfiguration(profile); + + // Ricarica i profili per aggiornare la data di ultimo utilizzo + await LoadProfiles(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento del profilo"); + await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento del profilo: {ex.Message}"); + } + } + + private async Task ApplyProfileConfiguration(DataCouplerProfile profile) + { + // Reset dello stato corrente + ResetAllState(); + + // Applica configurazione sorgente + selectedSourceType = profile.SourceType; + + if (profile.SourceCredentialId.HasValue) + { + // Per ora, uso il nome della credenziale come identificatore + // TODO: Implementare risoluzione corretta tramite ID quando disponibile + // In alternativa, potremmo aggiungere il nome della credenziale al profilo + + // Se c'è uno schema salvato nel profilo, utilizziamolo per la connessione + if (!string.IsNullOrEmpty(profile.SourceSchema)) + { + Logger.LogInformation("Applicando schema dal profilo: {Schema}", profile.SourceSchema); + // Prima verifichiamo che ci sia una credenziale selezionata + if (!string.IsNullOrEmpty(selectedDatabaseCredential)) + { + await ConnectToDatabaseWithSchema(profile.SourceSchema); + } + else + { + Logger.LogWarning("Nessuna credenziale database selezionata per applicare lo schema"); + } + } + else if (!string.IsNullOrEmpty(selectedDatabaseCredential)) + { + // Connetti al database senza schema specifico + await ConnectToDatabase(); + } + + // Seleziona la tabella se specificata + if (!string.IsNullOrEmpty(profile.SourceTable)) + { + selectedTable = profile.SourceTable; + } + } + + // Applica configurazione destinazione + if (profile.DestinationCredentialId.HasValue) + { + // Similmente, per ora gestiamo senza risoluzione diretta dell'ID + // TODO: Implementare risoluzione corretta tramite ID quando disponibile + + if (!string.IsNullOrEmpty(selectedRestCredential)) + { + // Connetti al servizio REST + await ConnectToRestApi(); + + // Trova e seleziona l'entità REST + if (!string.IsNullOrEmpty(profile.DestinationEndpoint)) + { + var entity = restEntities.FirstOrDefault(e => e.Name == profile.DestinationEndpoint); + if (entity != null) + { + await SelectRestEntity(entity); + } + else + { + Logger.LogWarning("Entità REST con endpoint {Endpoint} non trovata", profile.DestinationEndpoint); + } + } + } + } + + // Applica mapping dei campi se disponibile + if (!string.IsNullOrEmpty(profile.FieldMappingJson)) + { + try + { + var service = new DataCouplerProfileService(null!); // Temporaneo per deserializzazione + var mappings = service.DeserializeFieldMappings(profile.FieldMappingJson); + + // Applica i mapping + fieldMappings.Clear(); + keyFields.Clear(); + + foreach (var mapping in mappings) + { + fieldMappings[mapping.SourceField] = mapping.DestinationField; + if (mapping.IsKey) + { + keyFields.Add(mapping.DestinationField); + } + } + + Logger.LogInformation("Applicati {MappingCount} mapping dei campi dal profilo", mappings.Count); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Errore nel caricamento dei mapping dei campi dal profilo"); + } + } + + StateHasChanged(); + } + + private async Task OnProfileSaved(DataCouplerProfileDto profileDto) + { + try + { + var profileService = new DataCouplerProfileService(null!); // Usa il service di conversione + var profile = profileService.FromDto(profileDto, "System"); // TODO: Usa utente corrente + + await ProfileService.SaveProfileAsync(profile); + await LoadProfiles(); // Ricarica la lista + + await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' salvato con successo!"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel salvataggio del profilo"); + await JSRuntime.InvokeVoidAsync("alert", $"Errore nel salvataggio del profilo: {ex.Message}"); + } + } + + private async Task OnProfileDeleted(int profileId) + { + try + { + var deleted = await ProfileService.DeleteProfileAsync(profileId); + if (deleted) + { + await LoadProfiles(); // Ricarica la lista + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'eliminazione del profilo"); + throw; // Rilancia per gestire nell'UI + } + } + + private void OnManageProfiles() + { + showProfileManagement = true; + } + + private void OnCloseProfileManagement() + { + showProfileManagement = false; + } + + private bool CanSaveProfile() + { + return !string.IsNullOrEmpty(selectedSourceType) && + (!string.IsNullOrEmpty(selectedDatabaseCredential) || !string.IsNullOrEmpty(selectedRestCredential)) && + (!string.IsNullOrEmpty(selectedRestCredential) || !string.IsNullOrEmpty(selectedTable)); + } + + private List GetCurrentFieldMappings() + { + var mappings = new List(); + + foreach (var mapping in fieldMappings) + { + mappings.Add(new FieldMappingDto + { + SourceField = mapping.Key, + DestinationField = mapping.Value, + IsKey = keyFields.Contains(mapping.Value), + IsRequired = false, // TODO: Determina dai metadati + DataType = "", // TODO: Determina dai metadati + }); + } + + return mappings; + } + + private void ResetAllState() + { + ResetSourceState(); + ResetDestinationState(); + fieldMappings.Clear(); + keyFields.Clear(); + transferResults.Clear(); + transferMessage = ""; + } + + private void ResetDestinationState() + { + selectedRestCredential = ""; + isConnectingRest = false; + isRestConnected = false; + restErrorMessage = ""; + restEntities.Clear(); + selectedRestEntity = null; + restEntityDetails = null; + restSearchTerm = ""; + currentRestDiscovery = null; + currentRestClient = null; + } + private void OnSourceTypeChanged(ChangeEventArgs e) { selectedSourceType = e.Value?.ToString() ?? ""; @@ -564,7 +807,7 @@ public partial class DataCoupler if (databaseTables.Count == 0) { // Se non ci sono tabelle, potrebbe essere perché non è stato selezionato un database specifico - await HandleDatabaseSelectionRequired(); + HandleDatabaseSelectionRequired(); return; } } @@ -1103,7 +1346,6 @@ public partial class DataCoupler } } - CreateNewRecord: // Crea un nuovo record var result = await currentRestClient.CreateEntityAsync(selectedRestEntity.Name, restData); @@ -1526,235 +1768,322 @@ public partial class DataCoupler } } - private async Task HandleDatabaseSelectionRequired() + /// + /// Gestisce la connessione al database con schema specifico + /// + private async Task ConnectToDatabaseWithSchema(string? specificSchema = null) { + if (string.IsNullOrEmpty(selectedDatabaseCredential)) + return; + + isConnectingDatabase = true; + databaseErrorMessage = ""; + try { - if (currentDatabaseManager == null) + // Trova la credenziale + var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); + if (credential == null) { - databaseErrorMessage = "Database manager non inizializzato"; + databaseErrorMessage = "Credenziale database non trovata"; return; } - // Ottieni la lista dei database disponibili - availableDatabases = await currentDatabaseManager.GetAvailableDatabasesAsync(); - - if (availableDatabases != null && availableDatabases.Any()) + // Test della connessione + var (success, message) = await CredentialService.TestDatabaseConnectionAsync(credential.Name); + if (!success) { - // Mostra il modal per la selezione del database - showDatabaseSelectionModal = true; - StateHasChanged(); + databaseErrorMessage = $"Connessione fallita: {message}"; + return; + } + + // Crea il database manager + Logger.LogInformation("Creando database manager per credenziale: {CredentialName}", selectedDatabaseCredential); + currentDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDatabaseCredential); + Logger.LogInformation("Database manager creato con successo"); + + // Se è specificato uno schema, utilizzalo direttamente + if (!string.IsNullOrEmpty(specificSchema)) + { + Logger.LogInformation("Utilizzando schema specifico: {Schema}", specificSchema); + await LoadSchemaForDatabase(specificSchema); } else { - databaseErrorMessage = "Nessun database disponibile per la selezione"; + // Prova il discovery automatico dello schema + await DiscoverDatabaseSchema(); + } + + if (databaseTables.Count > 0) + { + isDatabaseConnected = true; + Logger.LogInformation("Connessione database completata con {TableCount} tabelle", databaseTables.Count); } } catch (Exception ex) { - Logger.LogError(ex, "Errore nell'ottenere la lista dei database disponibili"); - databaseErrorMessage = $"Errore nel recupero dei database: {ex.Message}"; + Logger.LogError(ex, "Errore nella connessione al database"); + databaseErrorMessage = $"Errore: {ex.Message}"; + } + finally + { + isConnectingDatabase = false; + StateHasChanged(); } } - private async Task OnDatabaseSelected() + /// + /// Scopre automaticamente lo schema del database + /// + private async Task DiscoverDatabaseSchema() { - if (string.IsNullOrEmpty(selectedDatabase)) - { - return; - } - - if (currentDatabaseManager == null) - { - databaseErrorMessage = "Database manager non inizializzato"; - return; - } - try { - // Cambia il database attivo - await currentDatabaseManager.ChangeDatabaseAsync(selectedDatabase); + Logger.LogInformation("Iniziando discovery automatico dello schema"); + var schema = await currentDatabaseManager!.GetDatabaseSchemaAsync(); - // Nasconde il modal - showDatabaseSelectionModal = false; - - // Ritenta il discovery dello schema - var schema = await currentDatabaseManager.GetDatabaseSchemaAsync(); + Logger.LogInformation("Schema discovery completato. Numero elementi: {Count}", schema?.Count() ?? 0); + databaseTables = schema as Dictionary> ?? (schema != null ? new Dictionary>(schema) : new Dictionary>()); if (databaseTables.Count == 0) { - databaseErrorMessage = $"Il database '{selectedDatabase}' non contiene tabelle accessibili"; + // Se non ci sono tabelle, potrebbe essere necessario selezionare un database specifico + HandleDatabaseSelectionRequired(); } else { - isDatabaseConnected = true; - databaseErrorMessage = ""; + // Rileva e salva lo schema corrente se presente nelle chiavi delle tabelle + var firstTableKey = databaseTables.Keys.FirstOrDefault(); + if (!string.IsNullOrEmpty(firstTableKey) && firstTableKey.Contains('.')) + { + var detectedSchema = firstTableKey.Split('.')[0]; + Logger.LogInformation("Schema rilevato automaticamente: {Schema}", detectedSchema); + } } } catch (Exception ex) { - Logger.LogError(ex, "Errore nel cambio di database a {Database}", selectedDatabase); - databaseErrorMessage = $"Errore nel cambio di database: {ex.Message}"; - } - finally - { - StateHasChanged(); + Logger.LogError(ex, "Errore durante il discovery automatico dello schema"); + throw; } } - private void CancelDatabaseSelection() + /// + /// Carica lo schema per un database specifico + /// + private async Task LoadSchemaForDatabase(string schemaName) { - showDatabaseSelectionModal = false; - selectedDatabase = ""; + try + { + // TODO: Implementare la logica specifica per il caricamento di uno schema + // Per ora utilizziamo il discovery standard e filtriamo i risultati + var schema = await currentDatabaseManager!.GetDatabaseSchemaAsync(); + + databaseTables = schema as Dictionary> ?? + new Dictionary>(); + + // Filtra le tabelle per lo schema specificato + if (!string.IsNullOrEmpty(schemaName)) + { + var filteredTables = databaseTables + .Where(kvp => kvp.Key.StartsWith($"{schemaName}.", StringComparison.OrdinalIgnoreCase)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + if (filteredTables.Any()) + { + databaseTables = filteredTables; + Logger.LogInformation("Caricate {TableCount} tabelle per lo schema {Schema}", filteredTables.Count, schemaName); + } + else + { + Logger.LogWarning("Nessuna tabella trovata per lo schema {Schema}", schemaName); + } + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento dello schema {Schema}", schemaName); + throw; + } + } + + /// + /// Gestisce la situazione quando è richiesta la selezione di un database specifico + /// + private void HandleDatabaseSelectionRequired() + { + try + { + // Prova a ottenere la lista dei database disponibili + // TODO: Implementare se il DatabaseManager supporta GetAvailableDatabases + Logger.LogInformation("Database selection richiesta - implementazione da completare"); + + // Per ora, impostiamo un messaggio di errore informativo + databaseErrorMessage = "Schema discovery non ha restituito risultati. Potrebbe essere necessario specificare un database o schema specifico nella connessione."; + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella gestione della selezione database"); + databaseErrorMessage = $"Errore nella selezione database: {ex.Message}"; + } + } + + /// + /// Estrae lo schema dal nome completo di una tabella + /// + private string? ExtractSchemaFromTableName(string fullTableName) + { + if (string.IsNullOrEmpty(fullTableName) || !fullTableName.Contains('.')) + return null; + + var parts = fullTableName.Split('.'); + return parts.Length > 1 ? parts[0] : null; + } + + /// + /// Ottiene lo schema correntemente utilizzato dal database connesso + /// + private string? GetCurrentDatabaseSchema() + { + if (!databaseTables.Any()) + return null; + + var firstTable = databaseTables.Keys.FirstOrDefault(); + return !string.IsNullOrEmpty(firstTable) ? ExtractSchemaFromTableName(firstTable) : null; + } + + /// + /// Ottiene il campo ID dell'entità REST selezionata + /// + private string GetEntityIdField() + { + if (restEntityDetails?.Properties != null) + { + // Cerca il campo ID (tipicamente "Id", "ID", "id", o il primo campo che contiene "id") + var idProperty = restEntityDetails.Properties.FirstOrDefault(p => + p.Name.Equals("Id", StringComparison.OrdinalIgnoreCase) || + p.Name.Equals("ID", StringComparison.OrdinalIgnoreCase) || + p.Name.Contains("id", StringComparison.OrdinalIgnoreCase)); + + return idProperty?.Name ?? "Id"; // Default a "Id" se non trovato + } + return "Id"; + } + + /// + /// Verifica se una query è una SELECT query sicura + /// + private bool IsSelectQuery(string query) + { + if (string.IsNullOrWhiteSpace(query)) + return false; + + var trimmedQuery = query.Trim(); + return trimmedQuery.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Pulisce una query SQL rimuovendo caratteri pericolosi + /// + private string CleanQuery(string query) + { + if (string.IsNullOrWhiteSpace(query)) + return ""; + + // Rimuove caratteri potenzialmente pericolosi + var cleanQuery = query.Trim(); + + // Rimuove eventuali terminatori multipli + while (cleanQuery.EndsWith(";")) + { + cleanQuery = cleanQuery.Substring(0, cleanQuery.Length - 1).Trim(); + } + + return cleanQuery; + } + + /// + /// Gestisce il cambio di modalità tra tabelle e query custom + /// + private void OnQueryModeChanged(ChangeEventArgs e) + { + useCustomQuery = (bool)(e.Value ?? false); + + // Reset stato quando cambia modalità + if (useCustomQuery) + { + // Reset selezione tabella + selectedTable = ""; + ClearAllMappings(); + } + else + { + // Reset query custom + customQuery = ""; + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + } + StateHasChanged(); } /// - /// Ottiene il nome del campo ID per l'entità corrente + /// Valida la query SQL custom /// - private string GetEntityIdField() - { - // Fallback predefiniti in base al tipo di servizio/entità - if (selectedRestEntity?.Name != null) - { - // Per SAP B1, la maggior parte delle entità usa DocEntry - if (selectedRestEntity.Name.Contains("BusinessPartner") || - selectedRestEntity.Name.Contains("Customer") || - selectedRestEntity.Name.Contains("Vendor")) - { - return "CardCode"; - } - - if (selectedRestEntity.Name.Contains("Item") || - selectedRestEntity.Name.Contains("Product")) - { - return "ItemCode"; - } - } - - // Usa campi ID comuni come fallback - var commonIdFields = new[] { "DocEntry", "Id", "ID", "id", "Key", "key", "Code", "code" }; - - // Per ora usa DocEntry come default per SAP B1 - return "DocEntry"; - } - - // Custom Query Methods - private void OnQueryModeChanged(ChangeEventArgs e) - { - useCustomQuery = bool.Parse(e.Value?.ToString() ?? "false"); - - if (useCustomQuery) - { - // Reset table selection when switching to custom query - selectedTable = ""; - ClearAllMappings(); - - // Reset query-specific state - customQuery = ""; - isQueryValid = false; - queryValidationMessage = ""; - queryPreviewData.Clear(); - queryColumns.Clear(); - showQueryPreview = false; - - // For custom queries, require manual key selection - sourceKeyField = ""; - suggestedPrimaryKey = ""; - requiresManualKeySelection = true; - } - else - { - // Reset custom query when switching to table mode - customQuery = ""; - isQueryValid = false; - queryValidationMessage = ""; - queryPreviewData.Clear(); - queryColumns.Clear(); - showQueryPreview = false; - ClearAllMappings(); - - // Reset key field selection - sourceKeyField = ""; - suggestedPrimaryKey = ""; - requiresManualKeySelection = false; - } - - StateHasChanged(); - } - private async Task ValidateCustomQuery() { if (string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null) { isQueryValid = false; - queryValidationMessage = "Query vuota o database non connesso"; - return; - } - - // CONTROLLO DI SICUREZZA: Verifica che sia una SELECT - if (!IsSelectQuery(customQuery)) - { - isQueryValid = false; - queryValidationMessage = "ERRORE DI SICUREZZA: Sono permesse solo query SELECT. Operazioni come INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE non sono consentite."; - Logger.LogWarning("Tentativo di eseguire query non SELECT bloccato: {Query}", customQuery.Length > 100 ? customQuery.Substring(0, 100) + "..." : customQuery); + queryValidationMessage = "Query vuota o manager database non disponibile"; return; } isValidatingQuery = true; - queryValidationMessage = ""; - queryColumns.Clear(); try { - // Converte la query per testare solo 1 riga - var testQuery = ConvertQueryForValidation(customQuery); + // Controllo di sicurezza: verifica che sia una SELECT + if (!IsSelectQuery(customQuery)) + { + isQueryValid = false; + queryValidationMessage = "Solo query SELECT sono permesse per sicurezza"; + return; + } + + var cleanQuery = CleanQuery(customQuery); - Logger.LogInformation("Validazione query: {TestQuery}", testQuery); - - // Esegue la query di test - var testResults = await currentDatabaseManager.ExecuteRawQueryAsync(testQuery); + // Prova a eseguire la query per validarla + var testResults = await currentDatabaseManager.ExecuteRawQueryAsync($"{cleanQuery} LIMIT 1"); if (testResults != null && testResults.Any()) { + var firstRow = testResults.First(); + queryColumns = firstRow.Keys.ToList(); isQueryValid = true; + queryValidationMessage = $"Query valida - {queryColumns.Count} colonne rilevate"; - // Estrae i nomi delle colonne dal primo record - var firstRecord = testResults.First(); - queryColumns = firstRecord.Keys.ToList(); + // Clear mappings quando cambia la query + ClearAllMappings(); - // Non mostra più messaggi di successo per ridurre l'ingombro visivo - queryValidationMessage = ""; - - Logger.LogInformation("Query validata con successo. Colonne: {Columns}", string.Join(", ", queryColumns)); - - // Clear existing mappings since we have new columns - fieldMappings.Clear(); - selectedDbColumn = ""; - selectedRestProperty = ""; - - // For custom queries, always require manual key selection - sourceKeyField = ""; - suggestedPrimaryKey = ""; - requiresManualKeySelection = true; - - StateHasChanged(); + Logger.LogInformation("Query validata con successo: {ColumnCount} colonne", queryColumns.Count); } else { isQueryValid = false; - queryValidationMessage = "Query valida ma non restituisce risultati"; - queryColumns.Clear(); + queryValidationMessage = "La query non ha restituito risultati o colonne"; } } catch (Exception ex) { + Logger.LogError(ex, "Errore nella validazione della query"); isQueryValid = false; queryValidationMessage = $"Errore nella validazione: {ex.Message}"; - queryColumns.Clear(); - Logger.LogError(ex, "Errore nella validazione della query custom"); } finally { @@ -1763,31 +2092,33 @@ public partial class DataCoupler } } + /// + /// Carica un'anteprima dei dati della query + /// private async Task LoadQueryPreview() { if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null) - { return; - } isLoadingPreview = true; try { - // Usa la query limitata per il preview (max 50 righe per performance) - var previewQuery = ConvertQueryForPreview(customQuery, 50); + var cleanQuery = CleanQuery(customQuery); - Logger.LogInformation("Caricamento preview query: {PreviewQuery}", previewQuery); + // Aggiungi LIMIT per l'anteprima + var previewQuery = $"{cleanQuery} LIMIT 10"; - queryPreviewData = await currentDatabaseManager.ExecuteRawQueryAsync(previewQuery); + var previewResults = await currentDatabaseManager.ExecuteRawQueryAsync(previewQuery); + queryPreviewData = previewResults.ToList(); showQueryPreview = true; - Logger.LogInformation("Preview caricato: {RowCount} righe", queryPreviewData.Count); + Logger.LogInformation("Caricata anteprima query con {RecordCount} record", queryPreviewData.Count); } catch (Exception ex) { - queryValidationMessage = $"Errore nel caricamento preview: {ex.Message}"; - Logger.LogError(ex, "Errore nel caricamento del preview della query"); + Logger.LogError(ex, "Errore nel caricamento anteprima query"); + queryValidationMessage = $"Errore anteprima: {ex.Message}"; } finally { @@ -1796,223 +2127,70 @@ public partial class DataCoupler } } + /// + /// Nasconde l'anteprima della query + /// private void HideQueryPreview() { showQueryPreview = false; - queryPreviewData.Clear(); StateHasChanged(); } - private string ConvertQueryForValidation(string originalQuery) + /// + /// Ottiene l'ID della credenziale sorgente corrente + /// + private int? GetCurrentSourceCredentialId() { - // Rimuove commenti e spazi extra - var cleanQuery = CleanQuery(originalQuery); - - // Se la query ha già un LIMIT/TOP, la usa così com'è per il test - if (HasLimitClause(cleanQuery)) - { - return cleanQuery; - } - - // Aggiunge LIMIT/TOP in base al tipo di database - return AddLimitClause(cleanQuery, 1); + // TODO: Implementare logica per ottenere l'ID dalla credenziale selezionata + // Per ora ritorniamo null dato che i DTO non hanno ID + return null; } - private string ConvertQueryForPreview(string originalQuery, int maxRows = 50) + /// + /// Ottiene l'ID della credenziale destinazione corrente + /// + private int? GetCurrentDestinationCredentialId() { - var cleanQuery = CleanQuery(originalQuery); - - // Se la query ha già un LIMIT/TOP con un valore minore, la mantiene - if (HasLimitClause(cleanQuery)) - { - return cleanQuery; - } - - return AddLimitClause(cleanQuery, maxRows); + // TODO: Implementare logica per ottenere l'ID dalla credenziale selezionata + // Per ora ritorniamo null dato che i DTO non hanno ID + return null; } - private string CleanQuery(string query) + /// + /// Annulla la selezione del database + /// + private void CancelDatabaseSelection() { - if (string.IsNullOrWhiteSpace(query)) - return ""; - - // Rimuove commenti SQL - var lines = query.Split('\n') - .Select(line => line.Contains("--") ? line.Substring(0, line.IndexOf("--")) : line) - .Where(line => !string.IsNullOrWhiteSpace(line)); - - var cleanQuery = string.Join(" ", lines).Trim(); - - // Rimuove il punto e virgola finale se presente - if (cleanQuery.EndsWith(";")) - { - cleanQuery = cleanQuery.Substring(0, cleanQuery.Length - 1); - } - - return cleanQuery; - } - - private bool HasLimitClause(string query) - { - var upperQuery = query.ToUpperInvariant(); - return upperQuery.Contains(" LIMIT ") || - upperQuery.Contains(" TOP ") || - upperQuery.Contains("ROWNUM") || - upperQuery.Contains("FETCH FIRST"); - } - - private string AddLimitClause(string query, int limit) - { - var upperQuery = query.ToUpperInvariant(); - - // Per SQL Server, Oracle, e altri che supportano TOP - if (upperQuery.Contains("SELECT ")) - { - var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); - if (credential != null) - { - var dbType = credential.DatabaseType.ToString().ToLowerInvariant(); - switch (dbType) - { - case "sqlserver": - case "oracle": - // Aggiunge TOP dopo SELECT - return query.Replace("SELECT ", $"SELECT TOP {limit} ", StringComparison.OrdinalIgnoreCase); - - case "mysql": - case "postgresql": - case "sqlite": - default: - // Aggiunge LIMIT alla fine - return $"{query} LIMIT {limit}"; - } - } - } - - // Fallback: aggiunge LIMIT - return $"{query} LIMIT {limit}"; - } - - private void OnCustomQueryChanged(ChangeEventArgs e) - { - customQuery = e.Value?.ToString() ?? ""; - - // Reset validation quando la query cambia - isQueryValid = false; - queryValidationMessage = ""; - queryPreviewData.Clear(); - queryColumns.Clear(); - showQueryPreview = false; - - // Clear mappings quando la query cambia - ClearAllMappings(); - - // Reset key field selection - sourceKeyField = ""; - suggestedPrimaryKey = ""; - requiresManualKeySelection = true; - + showDatabaseSelectionModal = false; + selectedDatabase = ""; StateHasChanged(); } - + /// - /// Verifica che la query sia una SELECT e non contenga operazioni pericolose + /// Conferma la selezione del database /// - private bool IsSelectQuery(string query) + private async Task OnDatabaseSelected() { - if (string.IsNullOrWhiteSpace(query)) - return false; + if (string.IsNullOrEmpty(selectedDatabase)) + return; + + showDatabaseSelectionModal = false; + + try + { + // TODO: Implementare la logica per connettersi al database selezionato + Logger.LogInformation("Database selezionato: {Database}", selectedDatabase); - // Rimuovi commenti e normalizza la query - var cleanQuery = CleanQueryForSecurityCheck(query); - - // Lista delle operazioni pericolose che non sono permesse - var dangerousKeywords = new[] + // Per ora, chiudi semplicemente il dialog + await Task.CompletedTask; + } + catch (Exception ex) { - "INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", - "TRUNCATE", "REPLACE", "MERGE", "EXEC", "EXECUTE", - "DECLARE", "SET", "GRANT", "REVOKE", "BACKUP", "RESTORE", - "SHUTDOWN", "KILL", "LOAD", "BULK", "OPENROWSET", "OPENDATASOURCE" - }; - - // Verifica che non contenga operazioni pericolose - foreach (var keyword in dangerousKeywords) - { - if (cleanQuery.Contains($" {keyword} ", StringComparison.OrdinalIgnoreCase) || - cleanQuery.StartsWith($"{keyword} ", StringComparison.OrdinalIgnoreCase) || - cleanQuery.Contains($";{keyword} ", StringComparison.OrdinalIgnoreCase) || - cleanQuery.Contains($"\n{keyword} ", StringComparison.OrdinalIgnoreCase) || - cleanQuery.Contains($"\r{keyword} ", StringComparison.OrdinalIgnoreCase)) - { - Logger.LogWarning("Query bloccata: contiene keyword pericolosa '{Keyword}' in query: {QueryStart}", - keyword, query.Length > 50 ? query.Substring(0, 50) + "..." : query); - return false; - } + Logger.LogError(ex, "Errore nella selezione del database"); + databaseErrorMessage = $"Errore nella selezione database: {ex.Message}"; } - // Verifica che inizi con SELECT (permettendo spazi e commenti iniziali) - var trimmedQuery = cleanQuery.TrimStart(); - if (!trimmedQuery.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)) - { - Logger.LogWarning("Query bloccata: non inizia con SELECT. Query: {QueryStart}", - query.Length > 50 ? query.Substring(0, 50) + "..." : query); - return false; - } - - // Verifica addizionale: non deve contenere punto e virgola seguito da altra query - var statements = cleanQuery.Split(';', StringSplitOptions.RemoveEmptyEntries); - if (statements.Length > 1) - { - // Se ci sono multiple statements, tutte devono essere SELECT o commenti vuoti - foreach (var statement in statements) - { - var trimmedStatement = statement.Trim(); - if (!string.IsNullOrEmpty(trimmedStatement) && - !trimmedStatement.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)) - { - Logger.LogWarning("Query bloccata: contiene multiple statements non SELECT. Query: {QueryStart}", - query.Length > 50 ? query.Substring(0, 50) + "..." : query); - return false; - } - } - } - - return true; - } - - /// - /// Pulisce la query per il controllo di sicurezza rimuovendo commenti - /// - private string CleanQueryForSecurityCheck(string query) - { - if (string.IsNullOrEmpty(query)) - return ""; - - var lines = query.Split('\n'); - var cleanedLines = new List(); - - foreach (var line in lines) - { - var cleanedLine = line.Trim(); - - // Rimuovi commenti SQL (-- e /* */) - var dashCommentIndex = cleanedLine.IndexOf("--"); - if (dashCommentIndex >= 0) - { - cleanedLine = cleanedLine.Substring(0, dashCommentIndex).Trim(); - } - - // Gestione commenti multiline /* */ - implementazione base - cleanedLine = System.Text.RegularExpressions.Regex.Replace(cleanedLine, @"/\*.*?\*/", " ", - System.Text.RegularExpressions.RegexOptions.IgnoreCase); - - if (!string.IsNullOrWhiteSpace(cleanedLine)) - { - cleanedLines.Add(cleanedLine); - } - } - - return string.Join(" ", cleanedLines); + StateHasChanged(); } } diff --git a/Data_Coupler/_Imports.razor b/Data_Coupler/_Imports.razor index 180d9ab..93c48d4 100644 --- a/Data_Coupler/_Imports.razor +++ b/Data_Coupler/_Imports.razor @@ -1,5 +1,4 @@ @using System.Net.Http -@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @@ -8,3 +7,4 @@ @using Microsoft.JSInterop @using Data_Coupler @using Data_Coupler.Shared +@using Components diff --git a/Data_Coupler/wwwroot/data/credentials.db b/Data_Coupler/wwwroot/data/credentials.db index aeb0244..cbf213e 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db and b/Data_Coupler/wwwroot/data/credentials.db differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-shm b/Data_Coupler/wwwroot/data/credentials.db-shm index c6bea81..fe9ac28 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-shm and b/Data_Coupler/wwwroot/data/credentials.db-shm differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-wal b/Data_Coupler/wwwroot/data/credentials.db-wal index 11e98fa..e69de29 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-wal and b/Data_Coupler/wwwroot/data/credentials.db-wal differ diff --git a/TestDataCouplerProfile/Program.cs b/TestDataCouplerProfile/Program.cs new file mode 100644 index 0000000..0857d7b --- /dev/null +++ b/TestDataCouplerProfile/Program.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DependencyInjection; +using CredentialManager; +using CredentialManager.Services; +using CredentialManager.Models; + +Console.WriteLine("🧪 Testing DataCouplerProfile Service..."); + +try +{ + // Crea un service provider con CredentialManager + var serviceProvider = await CredentialManagerFactory.CreateServiceProviderAsync(); + + // Ottieni il servizio per i profili + var profileService = serviceProvider.GetRequiredService(); + + Console.WriteLine("✅ Service created successfully!"); + + // Test: Ottieni tutti i profili (dovrebbe essere vuoto) + var profiles = await profileService.GetAllProfilesAsync(); + Console.WriteLine($"📋 Found {profiles.Count()} existing profiles"); + + // Test: Crea un profilo di test + var testProfile = new DataCouplerProfile + { + Name = "Test Profile", + Description = "Profile creato durante il test", + SourceType = "database", + DestinationType = "rest", + SourceSchema = "dbo", + SourceTable = "customers", + DestinationEndpoint = "/api/customers", + CreatedBy = "System Test" + }; + + // Salva il profilo + var savedProfile = await profileService.SaveProfileAsync(testProfile); + Console.WriteLine($"💾 Test profile saved with ID: {savedProfile.Id}"); + + // Ricarica i profili + profiles = await profileService.GetAllProfilesAsync(); + Console.WriteLine($"📋 Now found {profiles.Count()} profiles"); + + // Elimina il profilo di test + var deleted = await profileService.DeleteProfileAsync(savedProfile.Id); + Console.WriteLine($"🗑️ Test profile deleted: {deleted}"); + + Console.WriteLine("✅ All tests passed! DataCouplerProfile service is working correctly."); +} +catch (Exception ex) +{ + Console.WriteLine($"❌ Error during testing: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); +} + +Console.WriteLine("Press any key to exit..."); +Console.ReadKey(); diff --git a/TestDatabaseFix/TestDatabaseFix.csproj b/TestDataCouplerProfile/TestDataCouplerProfile.csproj similarity index 51% rename from TestDatabaseFix/TestDatabaseFix.csproj rename to TestDataCouplerProfile/TestDataCouplerProfile.csproj index 0933c15..1f690d8 100644 --- a/TestDatabaseFix/TestDatabaseFix.csproj +++ b/TestDataCouplerProfile/TestDataCouplerProfile.csproj @@ -3,6 +3,7 @@ Exe net9.0 + enable enable @@ -10,10 +11,4 @@ - - - - - - diff --git a/TestDatabaseFix/Program.cs b/TestDatabaseFix/Program.cs deleted file mode 100644 index 94b7e36..0000000 --- a/TestDatabaseFix/Program.cs +++ /dev/null @@ -1,50 +0,0 @@ -using CredentialManager.Data; -using CredentialManager.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace TestDatabaseFix; - -class Program -{ - static async Task Main(string[] args) - { - Console.WriteLine("Test Database Initialization Fix"); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddConsole()); - - // Configura il DbContext per usare SQLite - services.AddDbContext(options => - options.UseSqlite("Data Source=test_credentials.db")); - - services.AddScoped(); - - var serviceProvider = services.BuildServiceProvider(); - - using var scope = serviceProvider.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var initializer = scope.ServiceProvider.GetRequiredService(); - - try - { - Console.WriteLine("Inizializzando il database..."); - await initializer.InitializeAsync(); - - Console.WriteLine("Verifica tabelle..."); - var credentialsCount = await dbContext.Credentials.CountAsync(); - var associationsCount = await dbContext.RecordAssociations.CountAsync(); - - Console.WriteLine($"Tabella Credentials: {credentialsCount} record"); - Console.WriteLine($"Tabella RecordAssociations: {associationsCount} record"); - - Console.WriteLine("Test completato con successo!"); - } - catch (Exception ex) - { - Console.WriteLine($"Errore: {ex.Message}"); - Console.WriteLine($"Stack trace: {ex.StackTrace}"); - } - } -}