feat: Aggiunto sistema completo di gestione profili per Data Coupler

- Creata nuova libreria Components con componenti Blazor riutilizzabili
  * ProfileSelector: dropdown per selezione profili salvati
  * ProfileSaver: componente per salvare configurazioni correnti come profili
  * ProfileManagement: modale per gestione profili salvati
  * ProfileQuickActions: bottoni azioni rapide per operazioni sui profili

- Esteso CredentialManager con entità e servizi per DataCouplerProfile
  * Aggiunto modello DataCouplerProfile con configurazioni mapping e metadati
  * Implementata migrazione Entity Framework per memorizzazione profili
  * Creato DataCouplerProfileService per operazioni CRUD
  * Aggiunto CredentialDbContextFactory per operazioni database design-time

- Migliorato componente principale DataCoupler con integrazione profili
  * Integrata funzionalità caricamento/salvataggio profili
  * Aggiunto selettore profili nella parte superiore dell'interfaccia
  * Mantenuta retrocompatibilità con funzionalità esistenti
  * Migliorata esperienza utente con gestione configurazioni salvate

- Aggiornata struttura progetto e dipendenze
  * Aggiunto progetto Components alla soluzione
  * Aggiornati riferimenti progetti e import
  * Rimosso progetto obsoleto TestDatabaseFix

Questo aggiornamento migliora significativamente il flusso di lavoro permettendo agli utenti di salvare, caricare e gestire configurazioni complete di accoppiamento dati come
This commit is contained in:
2025-07-02 00:00:05 +02:00
parent 1435c013d3
commit 7e450a358b
34 changed files with 2430 additions and 422 deletions
+22
View File
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CredentialManager\CredentialManager.csproj" />
</ItemGroup>
</Project>
+180
View File
@@ -0,0 +1,180 @@
@* Componente per la gestione completa dei profili *@
<!-- Modal per la gestione profili -->
@if (ShowModal)
{
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-cogs"></i> Gestione Profili
</h5>
<button type="button" class="btn-close" @onclick="CloseModal"></button>
</div>
<div class="modal-body">
@if (IsLoading)
{
<div class="text-center py-4">
<div class="spinner-border" role="status">
<span class="visually-hidden">Caricamento...</span>
</div>
<p class="mt-2">Caricamento profili...</p>
</div>
}
else if (Profiles == null || !Profiles.Any())
{
<div class="text-center py-4">
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
<h6 class="text-muted">Nessun profilo salvato</h6>
<p class="text-muted">Configura una connessione e salva il tuo primo profilo!</p>
</div>
}
else
{
<!-- Filtro di ricerca -->
<div class="mb-3">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" @bind="SearchTerm" @oninput="FilterProfiles"
placeholder="Cerca profili per nome o descrizione..." />
</div>
</div>
<!-- Lista profili -->
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Nome</th>
<th>Fonte → Destinazione</th>
<th>Creato</th>
<th>Ultimo Uso</th>
<th width="120">Azioni</th>
</tr>
</thead>
<tbody>
@foreach (var profile in GetFilteredProfiles())
{
<tr>
<td>
<strong>@profile.Name</strong>
@if (!string.IsNullOrEmpty(profile.Description))
{
<br><small class="text-muted">@profile.Description</small>
}
</td>
<td>
<span class="badge bg-primary me-1">@GetTypeLabel(profile.SourceType)</span>
<i class="fas fa-arrow-right text-muted"></i>
<span class="badge bg-success ms-1">@GetTypeLabel(profile.DestinationType)</span>
<br>
<small class="text-muted">
@GetProfileSummary(profile)
</small>
</td>
<td>
<small>
@profile.CreatedAt.ToString("dd/MM/yyyy HH:mm")
@if (!string.IsNullOrEmpty(profile.CreatedBy))
{
<br><span class="text-muted">da @profile.CreatedBy</span>
}
</small>
</td>
<td>
<small>
@(profile.LastUsedAt?.ToString("dd/MM/yyyy HH:mm") ?? "Mai utilizzato")
</small>
</td>
<td>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary"
@onclick="() => LoadProfile(profile)"
title="Carica questo profilo">
<i class="fas fa-download"></i>
</button>
<button type="button" class="btn btn-outline-danger"
@onclick="() => ConfirmDelete(profile)"
title="Elimina questo profilo">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
@if (!GetFilteredProfiles().Any())
{
<div class="text-center py-3">
<i class="fas fa-search text-muted"></i>
<p class="text-muted mb-0">Nessun profilo corrisponde alla ricerca</p>
</div>
}
}
@if (!string.IsNullOrEmpty(Message))
{
<div class="alert alert-@(MessageType) mt-3">
<i class="fas fa-@(MessageType == "success" ? "check-circle" : "exclamation-triangle")"></i>
@Message
</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseModal">
<i class="fas fa-times"></i> Chiudi
</button>
</div>
</div>
</div>
</div>
}
<!-- Modal conferma eliminazione -->
@if (ShowDeleteConfirm && ProfileToDelete != null)
{
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.7);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-danger">
<i class="fas fa-exclamation-triangle"></i> Conferma Eliminazione
</h5>
</div>
<div class="modal-body">
<p>Sei sicuro di voler eliminare il profilo <strong>"@ProfileToDelete.Name"</strong>?</p>
@if (!string.IsNullOrEmpty(ProfileToDelete.Description))
{
<p class="text-muted">@ProfileToDelete.Description</p>
}
<div class="alert alert-warning">
<i class="fas fa-info-circle"></i>
<strong>Attenzione:</strong> Questa operazione non può essere annullata.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CancelDelete">
<i class="fas fa-times"></i> Annulla
</button>
<button type="button" class="btn btn-danger" @onclick="DeleteProfile" disabled="@IsDeleting">
@if (IsDeleting)
{
<span class="spinner-border spinner-border-sm" role="status"></span>
}
else
{
<i class="fas fa-trash"></i>
}
Elimina
</button>
</div>
</div>
</div>
</div>
}
+145
View File
@@ -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<DataCouplerProfile>? Profiles { get; set; }
[Parameter] public EventCallback OnCloseModal { get; set; }
[Parameter] public EventCallback<DataCouplerProfile> OnProfileLoaded { get; set; }
[Parameter] public EventCallback<int> 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<DataCouplerProfile> GetFilteredProfiles()
{
if (Profiles == null)
return Enumerable.Empty<DataCouplerProfile>();
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<string>();
// 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 = "";
}
}
+115
View File
@@ -0,0 +1,115 @@
@* Componente per salvare la configurazione corrente come profilo *@
<div class="card mb-3">
<div class="card-header bg-success text-white">
<h6 class="mb-0">
<i class="fas fa-save"></i> Salva Configurazione Corrente
</h6>
</div>
<div class="card-body">
@if (!ShowSaveForm)
{
<button type="button" class="btn btn-success" @onclick="ShowSaveDialog" disabled="@(!CanSave)">
<i class="fas fa-plus"></i> Salva come Nuovo Profilo
</button>
@if (!CanSave)
{
<small class="text-muted d-block mt-1">
<i class="fas fa-info-circle"></i>
Configura fonte e destinazione per abilitare il salvataggio
</small>
}
}
else
{
<EditForm Model="ProfileData" OnValidSubmit="SaveProfile">
<DataAnnotationsValidator />
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Nome Profilo <span class="text-danger">*</span></label>
<InputText @bind-Value="ProfileData.Name" class="form-control"
placeholder="Es: Export Clienti a CRM" />
<ValidationMessage For="@(() => ProfileData.Name)" />
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Descrizione</label>
<InputText @bind-Value="ProfileData.Description" class="form-control"
placeholder="Descrizione opzionale del profilo" />
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(SaveMessage))
{
<div class="alert alert-@(SaveMessageType) mb-3">
<i class="fas fa-@(SaveMessageType == "success" ? "check-circle" : "exclamation-triangle")"></i>
@SaveMessage
</div>
}
<!-- Anteprima Configurazione -->
<div class="mb-3">
<label class="form-label">Configurazione da salvare:</label>
<div class="bg-light p-3 rounded small">
<div class="row">
<div class="col-md-6">
<strong>Fonte:</strong> @GetSourceSummary()<br />
@if (!string.IsNullOrEmpty(SourceSchema))
{
<span class="text-muted">Schema: @SourceSchema</span><br />
}
@if (!string.IsNullOrEmpty(SourceTable))
{
<span class="text-muted">Tabella: @SourceTable</span>
}
</div>
<div class="col-md-6">
<strong>Destinazione:</strong> @GetDestinationSummary()<br />
@if (!string.IsNullOrEmpty(DestinationSchema))
{
<span class="text-muted">Schema: @DestinationSchema</span><br />
}
@if (!string.IsNullOrEmpty(DestinationTable))
{
<span class="text-muted">Tabella: @DestinationTable</span>
}
@if (!string.IsNullOrEmpty(DestinationEndpoint))
{
<span class="text-muted">Endpoint: @DestinationEndpoint</span>
}
</div>
</div>
@if (FieldMappings != null && FieldMappings.Any())
{
<hr class="my-2" />
<small class="text-muted">
<i class="fas fa-exchange-alt"></i>
@FieldMappings.Count mapping dei campi configurati
</small>
}
</div>
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary me-2" @onclick="CancelSave">
<i class="fas fa-times"></i> Annulla
</button>
<button type="submit" class="btn btn-success" disabled="@IsSaving">
@if (IsSaving)
{
<span class="spinner-border spinner-border-sm" role="status"></span>
}
else
{
<i class="fas fa-save"></i>
}
Salva Profilo
</button>
</div>
</EditForm>
}
</div>
</div>
+123
View File
@@ -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<FieldMappingDto>? FieldMappings { get; set; }
[Parameter] public EventCallback<DataCouplerProfileDto> 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; }
}
}
+56
View File
@@ -0,0 +1,56 @@
@* Componente per la selezione e caricamento dei profili *@
<div class="card mb-3">
<div class="card-header bg-info text-white">
<h6 class="mb-0">
<i class="fas fa-user-cog"></i> Gestione Profili
</h6>
</div>
<div class="card-body">
<div class="row">
<!-- Selezione Profilo Esistente -->
<div class="col-md-8">
<label class="form-label">Carica Profilo Salvato:</label>
<div class="input-group">
<select class="form-select" @onchange="OnProfileSelected">
<option value="">-- Seleziona un profilo --</option>
@if (Profiles != null)
{
@foreach (var profile in Profiles)
{
<option value="@profile.Id" selected="@(SelectedProfileId == profile.Id)">
@profile.Name @if (!string.IsNullOrEmpty(profile.Description)) { <span class="text-muted">- @profile.Description</span> }
</option>
}
}
</select>
<button type="button" class="btn btn-outline-primary" @onclick="LoadSelectedProfile"
disabled="@(SelectedProfileId == 0 || IsLoading)">
@if (IsLoading)
{
<span class="spinner-border spinner-border-sm" role="status"></span>
}
else
{
<i class="fas fa-download"></i>
}
Carica
</button>
</div>
@if (!string.IsNullOrEmpty(LoadMessage))
{
<div class="alert alert-@(LoadMessageType) alert-sm mt-2 mb-0">
<i class="fas fa-@(LoadMessageType == "success" ? "check-circle" : "exclamation-triangle")"></i>
@LoadMessage
</div>
}
</div>
<!-- Pulsante Gestione Profili -->
<div class="col-md-4 d-flex align-items-end">
<button type="button" class="btn btn-outline-secondary w-100" @onclick="OpenProfileManagement">
<i class="fas fa-cogs"></i> Gestisci Profili
</button>
</div>
</div>
</div>
</div>
+64
View File
@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Components;
using CredentialManager.Models;
namespace Components;
public partial class ProfileSelector
{
[Parameter] public List<DataCouplerProfile>? Profiles { get; set; }
[Parameter] public EventCallback<DataCouplerProfile> 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;
}
}
+4
View File
@@ -0,0 +1,4 @@
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Forms
@using CredentialManager.Models
@using CredentialManager.Services
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

@@ -40,6 +40,7 @@ public static class CredentialManagerConfiguration
services.AddScoped<IEncryptionService, EncryptionService>(); services.AddScoped<IEncryptionService, EncryptionService>();
services.AddScoped<ICredentialService, CredentialService>(); services.AddScoped<ICredentialService, CredentialService>();
services.AddScoped<IDatabaseInitializer, DatabaseInitializer>(); services.AddScoped<IDatabaseInitializer, DatabaseInitializer>();
services.AddScoped<IDataCouplerProfileService, DataCouplerProfileService>();
return services; return services;
} }
@@ -10,6 +10,7 @@ public class CredentialDbContext : DbContext
{ {
public DbSet<CredentialEntity> Credentials { get; set; } public DbSet<CredentialEntity> Credentials { get; set; }
public DbSet<KeyAssociation> KeyAssociations { get; set; } public DbSet<KeyAssociation> KeyAssociations { get; set; }
public DbSet<DataCouplerProfile> DataCouplerProfiles { get; set; }
public CredentialDbContext(DbContextOptions<CredentialDbContext> options) : base(options) public CredentialDbContext(DbContextOptions<CredentialDbContext> options) : base(options)
{ {
@@ -141,5 +142,80 @@ public class CredentialDbContext : DbContext
entity.HasIndex(e => e.CreatedAt); entity.HasIndex(e => e.CreatedAt);
entity.HasIndex(e => e.LastVerifiedAt); entity.HasIndex(e => e.LastVerifiedAt);
}); });
// Configurazione della tabella DataCouplerProfiles
modelBuilder.Entity<DataCouplerProfile>(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);
});
} }
} }
@@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace CredentialManager.Data;
/// <summary>
/// Factory per creare il DbContext durante la fase di design (migrations)
/// </summary>
public class CredentialDbContextFactory : IDesignTimeDbContextFactory<CredentialDbContext>
{
public CredentialDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<CredentialDbContext>();
// Usa un database SQLite temporaneo per le migrations
var connectionString = "Data Source=design_time_temp.db";
optionsBuilder.UseSqlite(connectionString);
return new CredentialDbContext(optionsBuilder.Options);
}
}
@@ -0,0 +1,326 @@
// <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.Migrations
{
[DbContext(typeof(CredentialDbContext))]
[Migration("20250701203438_AddDataCouplerProfiles")]
partial class AddDataCouplerProfiles
{
/// <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<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>("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>("SourceFilePath")
.HasMaxLength(500)
.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.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>("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<string>("KeyValue")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastVerifiedAt")
.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.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
}
}
}
@@ -0,0 +1,104 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CredentialManager.Migrations
{
/// <inheritdoc />
public partial class AddDataCouplerProfiles : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DataCouplerProfiles",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
SourceType = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
SourceCredentialId = table.Column<int>(type: "INTEGER", nullable: true),
SourceSchema = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
SourceTable = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
SourceFilePath = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
DestinationType = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
DestinationCredentialId = table.Column<int>(type: "INTEGER", nullable: true),
DestinationSchema = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
DestinationTable = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
DestinationEndpoint = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
FieldMappingJson = table.Column<string>(type: "TEXT", maxLength: 4000, nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
LastUsedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
IsActive = table.Column<bool>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DataCouplerProfiles");
}
}
}
@@ -15,7 +15,7 @@ namespace CredentialManager.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b => modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
{ {
@@ -123,6 +123,104 @@ namespace CredentialManager.Migrations
b.ToTable("Credentials", (string)null); 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>("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>("SourceFilePath")
.HasMaxLength(500)
.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.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 => modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -202,6 +300,23 @@ namespace CredentialManager.Migrations
b.ToTable("KeyAssociations", (string)null); 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 #pragma warning restore 612, 618
} }
} }
@@ -0,0 +1,73 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CredentialManager.Models;
/// <summary>
/// Modello per salvare le configurazioni dei profili di Data Coupler
/// </summary>
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; }
}
@@ -0,0 +1,60 @@
namespace CredentialManager.Models;
/// <summary>
/// DTO per la creazione/aggiornamento di un profilo DataCoupler
/// </summary>
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<FieldMappingDto>? FieldMappings { get; set; }
}
/// <summary>
/// DTO per il mapping dei campi
/// </summary>
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; }
}
/// <summary>
/// DTO per la visualizzazione di un profilo nella lista
/// </summary>
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; }
}
@@ -0,0 +1,234 @@
using Microsoft.EntityFrameworkCore;
using CredentialManager.Data;
using CredentialManager.Models;
using System.Text.Json;
namespace CredentialManager.Services;
/// <summary>
/// Implementazione del servizio per la gestione dei profili Data Coupler
/// </summary>
public class DataCouplerProfileService : IDataCouplerProfileService
{
private readonly CredentialDbContext _context;
public DataCouplerProfileService(CredentialDbContext context)
{
_context = context;
}
/// <summary>
/// Ottiene tutti i profili attivi
/// </summary>
public async Task<IEnumerable<DataCouplerProfile>> 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();
}
/// <summary>
/// Ottiene un profilo per ID
/// </summary>
public async Task<DataCouplerProfile?> GetProfileByIdAsync(int id)
{
return await _context.DataCouplerProfiles
.Include(p => p.SourceCredential)
.Include(p => p.DestinationCredential)
.FirstOrDefaultAsync(p => p.Id == id && p.IsActive);
}
/// <summary>
/// Ottiene un profilo per nome
/// </summary>
public async Task<DataCouplerProfile?> GetProfileByNameAsync(string name)
{
return await _context.DataCouplerProfiles
.Include(p => p.SourceCredential)
.Include(p => p.DestinationCredential)
.FirstOrDefaultAsync(p => p.Name == name && p.IsActive);
}
/// <summary>
/// Salva un nuovo profilo
/// </summary>
public async Task<DataCouplerProfile> SaveProfileAsync(DataCouplerProfile profile)
{
profile.CreatedAt = DateTime.UtcNow;
profile.IsActive = true;
_context.DataCouplerProfiles.Add(profile);
await _context.SaveChangesAsync();
return profile;
}
/// <summary>
/// Aggiorna un profilo esistente
/// </summary>
public async Task<DataCouplerProfile> 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;
}
/// <summary>
/// Elimina un profilo (soft delete)
/// </summary>
public async Task<bool> 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;
}
/// <summary>
/// Aggiorna la data di ultimo utilizzo di un profilo
/// </summary>
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();
}
}
/// <summary>
/// Verifica se esiste un profilo con il nome specificato
/// </summary>
public async Task<bool> 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();
}
/// <summary>
/// Serializza la lista di mapping dei campi in JSON
/// </summary>
public string SerializeFieldMappings(List<FieldMappingDto>? mappings)
{
if (mappings == null || !mappings.Any())
return string.Empty;
return JsonSerializer.Serialize(mappings, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
/// <summary>
/// Deserializza il JSON dei mapping dei campi
/// </summary>
public List<FieldMappingDto> DeserializeFieldMappings(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return new List<FieldMappingDto>();
try
{
return JsonSerializer.Deserialize<List<FieldMappingDto>>(json, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}) ?? new List<FieldMappingDto>();
}
catch
{
return new List<FieldMappingDto>();
}
}
/// <summary>
/// Converte un DataCouplerProfile in DTO
/// </summary>
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)
};
}
/// <summary>
/// Converte un DTO in DataCouplerProfile
/// </summary>
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
};
}
}
@@ -0,0 +1,49 @@
using CredentialManager.Models;
namespace CredentialManager.Services;
/// <summary>
/// Interfaccia per il servizio di gestione dei profili Data Coupler
/// </summary>
public interface IDataCouplerProfileService
{
/// <summary>
/// Ottiene tutti i profili attivi
/// </summary>
Task<IEnumerable<DataCouplerProfile>> GetAllProfilesAsync();
/// <summary>
/// Ottiene un profilo per ID
/// </summary>
Task<DataCouplerProfile?> GetProfileByIdAsync(int id);
/// <summary>
/// Ottiene un profilo per nome
/// </summary>
Task<DataCouplerProfile?> GetProfileByNameAsync(string name);
/// <summary>
/// Salva un nuovo profilo
/// </summary>
Task<DataCouplerProfile> SaveProfileAsync(DataCouplerProfile profile);
/// <summary>
/// Aggiorna un profilo esistente
/// </summary>
Task<DataCouplerProfile> UpdateProfileAsync(DataCouplerProfile profile);
/// <summary>
/// Elimina un profilo
/// </summary>
Task<bool> DeleteProfileAsync(int id);
/// <summary>
/// Aggiorna la data di ultimo utilizzo di un profilo
/// </summary>
Task UpdateLastUsedAsync(int id);
/// <summary>
/// Verifica se esiste un profilo con il nome specificato
/// </summary>
Task<bool> ProfileExistsAsync(string name, int? excludeId = null);
}
Binary file not shown.
+14
View File
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataConnection", "DataConne
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialManager", "CredentialManager\CredentialManager.csproj", "{30B369DE-A0BA-4AD7-8895-7BEBD244E782}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialManager", "CredentialManager\CredentialManager.csproj", "{30B369DE-A0BA-4AD7-8895-7BEBD244E782}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components", "Components\Components.csproj", "{B5114CAC-3E03-4150-B93C-652882F66CB7}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|x64.Build.0 = Release|Any CPU
{30B369DE-A0BA-4AD7-8895-7BEBD244E782}.Release|x86.ActiveCfg = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+1
View File
@@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DataConnection\DataConnection.csproj" /> <ProjectReference Include="..\DataConnection\DataConnection.csproj" />
<ProjectReference Include="..\CredentialManager\CredentialManager.csproj" /> <ProjectReference Include="..\CredentialManager\CredentialManager.csproj" />
<ProjectReference Include="..\Components\Components.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+39 -1
View File
@@ -15,6 +15,7 @@
@inject IDataConnectionFactory ConnectionFactory @inject IDataConnectionFactory ConnectionFactory
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@inject ILogger<DataCoupler> Logger @inject ILogger<DataCoupler> Logger
@inject CredentialManager.Services.IDataCouplerProfileService ProfileService
<PageTitle>Data Coupler</PageTitle> <PageTitle>Data Coupler</PageTitle>
@@ -24,7 +25,19 @@
<h3><i class="fas fa-exchange-alt"></i> Data Coupler - Coupling Database e REST API</h3> <h3><i class="fas fa-exchange-alt"></i> Data Coupler - Coupling Database e REST API</h3>
<p class="text-muted">Connetti database e servizi REST per il trasferimento dati</p> <p class="text-muted">Connetti database e servizi REST per il trasferimento dati</p>
</div> </div>
</div> <div class="row"> </div>
<!-- Sezione Gestione Profili -->
<div class="row mb-3">
<div class="col-12">
<ProfileSelector Profiles="availableProfiles"
OnProfileLoaded="OnProfileLoaded"
OnManageProfiles="OnManageProfiles"
IsLoading="isLoadingProfiles" />
</div>
</div>
<div class="row">
<!-- Lato Sinistro - Fonte Dati --> <!-- Lato Sinistro - Fonte Dati -->
<div class="col-md-6"> <div class="col-md-6">
<div class="card h-100"> <div class="card h-100">
@@ -1103,6 +1116,31 @@
} }
</div> </div>
<!-- Sezione Salvataggio Profilo -->
@if (isDatabaseConnected && isRestConnected && fieldMappings.Any())
{
<div class="row mt-4">
<div class="col-12">
<ProfileSaver CanSave="CanSaveProfile()"
SourceType="selectedSourceType"
SourceSchema="@(databaseTables.Keys.FirstOrDefault()?.Split('.').FirstOrDefault())"
SourceTable="selectedTable"
DestinationType="rest"
DestinationEndpoint="@(selectedRestEntity?.Name)"
FieldMappings="GetCurrentFieldMappings()"
OnProfileSaved="OnProfileSaved" />
</div>
</div>
}
<!-- Componente Gestione Profili -->
<ProfileManagement ShowModal="showProfileManagement"
Profiles="availableProfiles"
OnCloseModal="OnCloseProfileManagement"
OnProfileLoaded="OnProfileLoaded"
OnProfileDeleted="OnProfileDeleted"
IsLoading="isLoadingProfiles" />
<!-- Modal per la selezione del database --> <!-- Modal per la selezione del database -->
@if (showDatabaseSelectionModal) @if (showDatabaseSelectionModal)
{ {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,4 @@
@using System.Net.Http @using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@@ -8,3 +7,4 @@
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Data_Coupler @using Data_Coupler
@using Data_Coupler.Shared @using Data_Coupler.Shared
@using Components
Binary file not shown.
Binary file not shown.
Binary file not shown.
+56
View File
@@ -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<IDataCouplerProfileService>();
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();
@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
@@ -10,10 +11,4 @@
<ProjectReference Include="..\CredentialManager\CredentialManager.csproj" /> <ProjectReference Include="..\CredentialManager\CredentialManager.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
</ItemGroup>
</Project> </Project>
-50
View File
@@ -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<CredentialDbContext>(options =>
options.UseSqlite("Data Source=test_credentials.db"));
services.AddScoped<DatabaseInitializer>();
var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<CredentialDbContext>();
var initializer = scope.ServiceProvider.GetRequiredService<DatabaseInitializer>();
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}");
}
}
}