[Feature] Implementazione completa supporto ODBC
- Aggiunta persistenza campi ODBC (OdbcDsnName, OdbcMode) in CredentialEntity - Creata migration EF Core per nuovi campi database - Aggiornato mapping credenziali per caricare/salvare dati ODBC - Creato OdbcDatabaseManager dedicato (bypass EF Core che non supporta ODBC) - Aggiornato DataConnectionFactory per usare OdbcDatabaseManager con connessioni ODBC - Fix auto-load DSN: sostituito @onchange con @bind-Value:after in dropdown tipo database - Fix test connessione SAP HANA: rimossa query SELECT 1 che causava errori sintassi - Implementati tutti i metodi IDatabaseManager in OdbcDatabaseManager - Supporto completo per discovery schema, tabelle e query ODBC Risolve problema DbContext non configurato per ODBC e abilita connessioni ODBC complete.
This commit is contained in:
Generated
+593
@@ -0,0 +1,593 @@
|
||||
// <auto-generated />
|
||||
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.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(CredentialDbContext))]
|
||||
[Migration("20260202165251_AddOdbcFieldsToCredentialEntity")]
|
||||
partial class AddOdbcFieldsToCredentialEntity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AdditionalParameters")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CommandTimeout")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30);
|
||||
|
||||
b.Property<string>("ConnectionString")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DatabaseName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DatabaseType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedApiKey")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedAuthToken")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedPassword")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Headers")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IgnoreSslErrors")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OdbcDsnName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OdbcMode")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Port")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RestServiceType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TimeoutSeconds")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(100);
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeletionAction")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeletionMarkField")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeletionMarkValue")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DestinationCredentialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DestinationEndpoint")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationSchema")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationTable")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FieldMappingJson")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<DateTime?>("LastUsedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SourceCredentialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SourceCustomQuery")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceDatabaseName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceKeyField")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceSchema")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceTable")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("SyncDeletions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AdditionalInfo")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data_Hash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DeletionSynced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DeletionSyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationEntity")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationKeyField")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsSourceDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyValue")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastVerifiedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MappedDestinationField")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RestCredentialName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceKeyField")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourcesInfo")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DailyTime")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DayOfMonth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("DayOfWeek")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationDatabaseOverride")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EnableDeletionSync")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ExecutionCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("IntervalUnit")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("IntervalValue")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LastExecutionMessage")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("LastExecutionRecordCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LastExecutionStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastExecutionTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("NextExecutionTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ScheduleType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ScheduledDateTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceDatabaseOverride")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileId");
|
||||
|
||||
b.ToTable("ProfileSchedules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AdditionalInfo")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationInfo")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ErrorDetails")
|
||||
.HasMaxLength(5000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ProfileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RecordsProcessed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("RecordsWithErrors")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SourceInfo")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TriggerType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CredentialManager.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddOdbcFieldsToCredentialEntity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "OdbcDsnName",
|
||||
table: "Credentials",
|
||||
type: "TEXT",
|
||||
maxLength: 100,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "OdbcMode",
|
||||
table: "Credentials",
|
||||
type: "TEXT",
|
||||
maxLength: 20,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OdbcDsnName",
|
||||
table: "Credentials");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OdbcMode",
|
||||
table: "Credentials");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,14 @@ namespace CredentialManager.Migrations
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OdbcDsnName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OdbcMode")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Port")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -61,6 +61,13 @@ public class CredentialEntity
|
||||
[MaxLength(2000)]
|
||||
public string? AdditionalParameters { get; set; } // JSON per parametri aggiuntivi
|
||||
|
||||
// ODBC specific fields
|
||||
[MaxLength(100)]
|
||||
public string? OdbcDsnName { get; set; } // Nome del DSN ODBC configurato
|
||||
|
||||
[MaxLength(20)]
|
||||
public string? OdbcMode { get; set; } // Dsn o Custom (OdbcConnectionMode enum)
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
@@ -33,7 +33,24 @@ public enum DatabaseType
|
||||
Oracle,
|
||||
Sqlite,
|
||||
DB2,
|
||||
SapHana
|
||||
SapHana,
|
||||
Odbc
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modalità di connessione ODBC
|
||||
/// </summary>
|
||||
public enum OdbcConnectionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Utilizzo di un DSN (Data Source Name) configurato
|
||||
/// </summary>
|
||||
Dsn,
|
||||
|
||||
/// <summary>
|
||||
/// Costruzione manuale della connection string
|
||||
/// </summary>
|
||||
Custom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -52,6 +69,10 @@ public class DatabaseCredential
|
||||
public int CommandTimeout { get; set; } = 30;
|
||||
public bool IgnoreSslErrors { get; set; } = false;
|
||||
public Dictionary<string, string>? AdditionalParameters { get; set; }
|
||||
|
||||
// ODBC specific properties
|
||||
public string? OdbcDsnName { get; set; } // Nome del DSN ODBC (se utilizzato)
|
||||
public OdbcConnectionMode OdbcMode { get; set; } = OdbcConnectionMode.Dsn; // Modalità ODBC (DSN o Custom)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -148,6 +169,7 @@ public static class ConnectionStringBuilder
|
||||
DatabaseType.Sqlite => BuildSqliteConnectionString(credential),
|
||||
DatabaseType.DB2 => BuildDb2ConnectionString(credential),
|
||||
DatabaseType.SapHana => BuildSapHanaConnectionString(credential),
|
||||
DatabaseType.Odbc => BuildOdbcConnectionString(credential),
|
||||
_ => throw new NotSupportedException($"Database type {credential.DatabaseType} not supported")
|
||||
};
|
||||
} private static string BuildSqlServerConnectionString(DatabaseCredential credential)
|
||||
@@ -275,6 +297,74 @@ public static class ConnectionStringBuilder
|
||||
return string.Join(";", builder);
|
||||
}
|
||||
|
||||
private static string BuildOdbcConnectionString(DatabaseCredential credential)
|
||||
{
|
||||
// Se è già presente una connection string personalizzata, utilizzala
|
||||
if (!string.IsNullOrEmpty(credential.ConnectionString))
|
||||
return credential.ConnectionString;
|
||||
|
||||
var builder = new List<string>();
|
||||
|
||||
// Modalità DSN: usa il DSN configurato
|
||||
if (credential.OdbcMode == OdbcConnectionMode.Dsn && !string.IsNullOrEmpty(credential.OdbcDsnName))
|
||||
{
|
||||
builder.Add($"DSN={credential.OdbcDsnName}");
|
||||
|
||||
// Aggiungi credenziali se fornite
|
||||
if (!string.IsNullOrEmpty(credential.Username))
|
||||
builder.Add($"UID={credential.Username}");
|
||||
|
||||
if (!string.IsNullOrEmpty(credential.Password))
|
||||
builder.Add($"PWD={credential.Password}");
|
||||
}
|
||||
// Modalità Custom: costruisci manualmente la connection string
|
||||
else
|
||||
{
|
||||
// Driver (se specificato nei parametri aggiuntivi)
|
||||
if (credential.AdditionalParameters?.ContainsKey("Driver") == true)
|
||||
{
|
||||
builder.Add($"Driver={{{credential.AdditionalParameters["Driver"]}}}");
|
||||
}
|
||||
|
||||
// Server/Host
|
||||
if (!string.IsNullOrEmpty(credential.Host))
|
||||
{
|
||||
builder.Add($"Server={credential.Host}");
|
||||
|
||||
// Porta (se diversa da 0)
|
||||
if (credential.Port > 0)
|
||||
builder.Add($"Port={credential.Port}");
|
||||
}
|
||||
|
||||
// Database
|
||||
if (!string.IsNullOrEmpty(credential.DatabaseName))
|
||||
builder.Add($"Database={credential.DatabaseName}");
|
||||
|
||||
// Credenziali
|
||||
if (!string.IsNullOrEmpty(credential.Username))
|
||||
builder.Add($"UID={credential.Username}");
|
||||
|
||||
if (!string.IsNullOrEmpty(credential.Password))
|
||||
builder.Add($"PWD={credential.Password}");
|
||||
}
|
||||
|
||||
// Timeout
|
||||
if (credential.CommandTimeout > 0)
|
||||
builder.Add($"Connection Timeout={credential.CommandTimeout}");
|
||||
|
||||
// Parametri aggiuntivi (escludendo Driver se già aggiunto)
|
||||
if (credential.AdditionalParameters != null)
|
||||
{
|
||||
foreach (var param in credential.AdditionalParameters)
|
||||
{
|
||||
if (param.Key != "Driver") // Driver già gestito sopra
|
||||
builder.Add($"{param.Key}={param.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join(";", builder);
|
||||
}
|
||||
|
||||
private static void AddAdditionalParameters(List<string> builder, Dictionary<string, string>? additionalParams)
|
||||
{
|
||||
if (additionalParams != null)
|
||||
|
||||
@@ -89,6 +89,8 @@ public class CredentialService : ICredentialService
|
||||
AdditionalParameters = credential.AdditionalParameters != null
|
||||
? JsonSerializer.Serialize(credential.AdditionalParameters)
|
||||
: null,
|
||||
OdbcDsnName = credential.OdbcDsnName,
|
||||
OdbcMode = credential.OdbcMode.ToString(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = Environment.UserName
|
||||
};
|
||||
@@ -110,6 +112,8 @@ public class CredentialService : ICredentialService
|
||||
existing.CommandTimeout = entity.CommandTimeout;
|
||||
existing.IgnoreSslErrors = entity.IgnoreSslErrors;
|
||||
existing.AdditionalParameters = entity.AdditionalParameters;
|
||||
existing.OdbcDsnName = entity.OdbcDsnName;
|
||||
existing.OdbcMode = entity.OdbcMode;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
_context.Credentials.Update(existing);
|
||||
@@ -695,7 +699,11 @@ public class CredentialService : ICredentialService
|
||||
Password = DecryptSafely(entity.EncryptedPassword, entity.Name, "password"),
|
||||
ConnectionString = entity.ConnectionString,
|
||||
CommandTimeout = entity.CommandTimeout,
|
||||
IgnoreSslErrors = entity.IgnoreSslErrors
|
||||
IgnoreSslErrors = entity.IgnoreSslErrors,
|
||||
OdbcDsnName = entity.OdbcDsnName,
|
||||
OdbcMode = !string.IsNullOrEmpty(entity.OdbcMode) && Enum.TryParse<OdbcConnectionMode>(entity.OdbcMode, out var odbcMode)
|
||||
? odbcMode
|
||||
: OdbcConnectionMode.Dsn
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(entity.AdditionalParameters))
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CredentialManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Informazioni su un DSN ODBC
|
||||
/// </summary>
|
||||
public class OdbcDsnInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Driver { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public bool IsUserDsn { get; set; } // true = User DSN, false = System DSN
|
||||
public Dictionary<string, string> Properties { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interfaccia per il servizio di discovery DSN ODBC
|
||||
/// </summary>
|
||||
public interface IOdbcDsnDiscoveryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Ottiene tutti i DSN ODBC configurati (sia User che System)
|
||||
/// </summary>
|
||||
List<OdbcDsnInfo> GetAllDsn();
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene solo i DSN utente
|
||||
/// </summary>
|
||||
List<OdbcDsnInfo> GetUserDsn();
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene solo i DSN di sistema
|
||||
/// </summary>
|
||||
List<OdbcDsnInfo> GetSystemDsn();
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene i dettagli di un DSN specifico
|
||||
/// </summary>
|
||||
OdbcDsnInfo? GetDsnDetails(string dsnName, bool isUserDsn = true);
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene la lista dei driver ODBC installati
|
||||
/// </summary>
|
||||
List<string> GetInstalledDrivers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Servizio per la scoperta e lettura dei DSN ODBC configurati sul sistema
|
||||
/// </summary>
|
||||
public class OdbcDsnDiscoveryService : IOdbcDsnDiscoveryService
|
||||
{
|
||||
private readonly ILogger<OdbcDsnDiscoveryService> _logger;
|
||||
|
||||
// Percorsi del registro di Windows per ODBC
|
||||
private const string USER_DSN_PATH = @"SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources";
|
||||
private const string SYSTEM_DSN_PATH = @"SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources";
|
||||
private const string USER_DSN_DETAILS_PATH = @"SOFTWARE\ODBC\ODBC.INI\";
|
||||
private const string SYSTEM_DSN_DETAILS_PATH = @"SOFTWARE\ODBC\ODBC.INI\";
|
||||
private const string DRIVERS_PATH = @"SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers";
|
||||
|
||||
public OdbcDsnDiscoveryService(ILogger<OdbcDsnDiscoveryService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<OdbcDsnInfo> GetAllDsn()
|
||||
{
|
||||
var allDsn = new List<OdbcDsnInfo>();
|
||||
allDsn.AddRange(GetUserDsn());
|
||||
allDsn.AddRange(GetSystemDsn());
|
||||
return allDsn;
|
||||
}
|
||||
|
||||
public List<OdbcDsnInfo> GetUserDsn()
|
||||
{
|
||||
return GetDsnFromRegistry(Registry.CurrentUser, USER_DSN_PATH, USER_DSN_DETAILS_PATH, true);
|
||||
}
|
||||
|
||||
public List<OdbcDsnInfo> GetSystemDsn()
|
||||
{
|
||||
return GetDsnFromRegistry(Registry.LocalMachine, SYSTEM_DSN_PATH, SYSTEM_DSN_DETAILS_PATH, false);
|
||||
}
|
||||
|
||||
public OdbcDsnInfo? GetDsnDetails(string dsnName, bool isUserDsn = true)
|
||||
{
|
||||
var allDsn = isUserDsn ? GetUserDsn() : GetSystemDsn();
|
||||
return allDsn.FirstOrDefault(d => d.Name.Equals(dsnName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public List<string> GetInstalledDrivers()
|
||||
{
|
||||
var drivers = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.OpenSubKey(DRIVERS_PATH);
|
||||
if (key != null)
|
||||
{
|
||||
foreach (var driverName in key.GetValueNames())
|
||||
{
|
||||
var value = key.GetValue(driverName)?.ToString();
|
||||
if (value == "Installed")
|
||||
{
|
||||
drivers.Add(driverName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore nella lettura dei driver ODBC dal registro");
|
||||
}
|
||||
|
||||
return drivers.OrderBy(d => d).ToList();
|
||||
}
|
||||
|
||||
private List<OdbcDsnInfo> GetDsnFromRegistry(RegistryKey rootKey, string dsnPath, string detailsPath, bool isUserDsn)
|
||||
{
|
||||
var dsnList = new List<OdbcDsnInfo>();
|
||||
|
||||
try
|
||||
{
|
||||
using var dsnKey = rootKey.OpenSubKey(dsnPath);
|
||||
if (dsnKey == null)
|
||||
{
|
||||
_logger.LogWarning("Chiave registro ODBC non trovata: {Path}", dsnPath);
|
||||
return dsnList;
|
||||
}
|
||||
|
||||
foreach (var dsnName in dsnKey.GetValueNames())
|
||||
{
|
||||
try
|
||||
{
|
||||
var driver = dsnKey.GetValue(dsnName)?.ToString();
|
||||
if (string.IsNullOrEmpty(driver))
|
||||
continue;
|
||||
|
||||
var dsnInfo = new OdbcDsnInfo
|
||||
{
|
||||
Name = dsnName,
|
||||
Driver = driver,
|
||||
IsUserDsn = isUserDsn
|
||||
};
|
||||
|
||||
// Leggi i dettagli del DSN
|
||||
using var detailKey = rootKey.OpenSubKey(detailsPath + dsnName);
|
||||
if (detailKey != null)
|
||||
{
|
||||
foreach (var valueName in detailKey.GetValueNames())
|
||||
{
|
||||
var value = detailKey.GetValue(valueName)?.ToString();
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
dsnInfo.Properties[valueName] = value;
|
||||
|
||||
// Popola proprietà comuni
|
||||
if (valueName.Equals("Description", StringComparison.OrdinalIgnoreCase))
|
||||
dsnInfo.Description = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dsnList.Add(dsnInfo);
|
||||
_logger.LogDebug("DSN trovato: {Name} ({Driver}) - Type: {Type}",
|
||||
dsnName, driver, isUserDsn ? "User" : "System");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Errore nella lettura del DSN: {DsnName}", dsnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore nella lettura dei DSN ODBC dal registro");
|
||||
}
|
||||
|
||||
return dsnList;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user