Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b75e57fe31 | |||
| 9fab99112b |
@@ -13,6 +13,44 @@
|
|||||||
- **Backup e Ripristino**: Sistema completo di backup/restore per configurazioni e dati
|
- **Backup e Ripristino**: Sistema completo di backup/restore per configurazioni e dati
|
||||||
- **Amministrazione Avanzata**: Interfaccia unificata per gestione sistema e sicurezza
|
- **Amministrazione Avanzata**: Interfaccia unificata per gestione sistema e sicurezza
|
||||||
|
|
||||||
|
## 🚀 **NUOVE FUNZIONALITÀ - Salesforce OAuth2 Client Credentials Flow (2026)**
|
||||||
|
|
||||||
|
### Supporto `grant_type=client_credentials` per autenticazione server-to-server
|
||||||
|
**Data Aggiornamento**: 2026
|
||||||
|
|
||||||
|
#### **Panoramica**
|
||||||
|
Aggiunto supporto per il flusso OAuth2 `client_credentials` come alternativa al flusso `password` già esistente.
|
||||||
|
Completamente retrocompatibile: il default rimane `Password`.
|
||||||
|
|
||||||
|
#### **Enum `SalesforceGrantType`** (in `CredentialManager/Models/CredentialModels.cs`)
|
||||||
|
```csharp
|
||||||
|
public enum SalesforceGrantType
|
||||||
|
{
|
||||||
|
Password, // grant_type=password — richiede Username, Password, SecurityToken (+ClientId/ClientSecret)
|
||||||
|
ClientCredentials // grant_type=client_credentials — server-to-server, nessun utente
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Differenze tra i flussi**
|
||||||
|
| Aspetto | `password` | `client_credentials` |
|
||||||
|
|---|---|---|
|
||||||
|
| Richiede Username/Password | ✅ Sì | ❌ No |
|
||||||
|
| Richiede SecurityToken | ✅ Sì (se non Connected App) | ❌ No |
|
||||||
|
| ClientId / ClientSecret | Opzionale | ✅ Obbligatorio |
|
||||||
|
| Base URL | login/test.salesforce.com | **My Domain URL** (es. `https://myorg.my.salesforce.com`) |
|
||||||
|
| Utente Salesforce | Necessario | Integration User (assegnato nella Connected App) |
|
||||||
|
|
||||||
|
#### **File modificati**
|
||||||
|
- `CredentialManager/Models/CredentialModels.cs` — enum `SalesforceGrantType`, proprietà `GrantType` su `RestApiCredential` e `SalesforceCredential`
|
||||||
|
- `DataConnection/REST/Configuration/RestServiceOptions.cs` — proprietà `SalesforceGrantType`
|
||||||
|
- `DataConnection/REST/Implementations/SalesforceServiceClient.cs` — `AuthenticateWithPasswordAsync`, `AuthenticateWithClientCredentialsAsync`, `SendTokenRequestAsync`; `ILogger` iniettato; `NormalizeNumericValues` reso non-static
|
||||||
|
- `Data_Coupler/Services/DataConnectionFactory.cs` — mapping `GrantType`, logger passato al client
|
||||||
|
- `CredentialManager/Services/CredentialService.cs` — `GrantType` serializzato/deserializzato in `AdditionalParameters` JSON
|
||||||
|
- `DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs` — `TestSalesforceOAuthLogin` instrada per `GrantType`
|
||||||
|
- `Data_Coupler/Pages/CredentialManagement.razor` — dropdown "Tipo di Autenticazione OAuth2"; Username/Password/SecurityToken nascosti per `ClientCredentials`; warning My Domain URL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🚀 **NUOVE FUNZIONALITÀ - Salesforce Optimizations (Febbraio 2026)**
|
## 🚀 **NUOVE FUNZIONALITÀ - Salesforce Optimizations (Febbraio 2026)**
|
||||||
|
|
||||||
### Salesforce Batch Describe via Composite API
|
### Salesforce Batch Describe via Composite API
|
||||||
|
|||||||
@@ -22,6 +22,27 @@ public enum RestServiceType
|
|||||||
Salesforce
|
Salesforce
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tipo di flusso OAuth2 per Salesforce
|
||||||
|
/// </summary>
|
||||||
|
public enum SalesforceGrantType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Flusso Username/Password (grant_type=password).
|
||||||
|
/// Richiede: ClientId, ClientSecret, Username, Password (+SecurityToken se non IP-trusted).
|
||||||
|
/// URL di login: https://login.salesforce.com o https://test.salesforce.com.
|
||||||
|
/// </summary>
|
||||||
|
Password,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flusso Client Credentials (grant_type=client_credentials) — server-to-server, senza utente.
|
||||||
|
/// Richiede: ClientId, ClientSecret.
|
||||||
|
/// URL obbligatorio: My Domain URL (es. https://myorg.my.salesforce.com).
|
||||||
|
/// La Connected App deve avere "Enable Client Credentials Flow" attivato e un Integration User assegnato.
|
||||||
|
/// </summary>
|
||||||
|
ClientCredentials
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tipi di database supportati (allineato con DataConnection.Enums.DatabaseType)
|
/// Tipi di database supportati (allineato con DataConnection.Enums.DatabaseType)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -106,6 +127,7 @@ public class RestApiCredential
|
|||||||
public string? ApiVersion { get; set; } = "59.0";
|
public string? ApiVersion { get; set; } = "59.0";
|
||||||
public bool IsSandbox { get; set; } = false;
|
public bool IsSandbox { get; set; } = false;
|
||||||
public bool UseSoapApi { get; set; } = false;
|
public bool UseSoapApi { get; set; } = false;
|
||||||
|
public SalesforceGrantType GrantType { get; set; } = SalesforceGrantType.Password;
|
||||||
public string? RefreshToken { get; set; }
|
public string? RefreshToken { get; set; }
|
||||||
public string? AccessToken { get; set; }
|
public string? AccessToken { get; set; }
|
||||||
public DateTime? TokenExpiry { get; set; }
|
public DateTime? TokenExpiry { get; set; }
|
||||||
@@ -145,6 +167,8 @@ public class SalesforceCredential
|
|||||||
public bool IsSandbox { get; set; } = false; // Se è un ambiente sandbox
|
public bool IsSandbox { get; set; } = false; // Se è un ambiente sandbox
|
||||||
public int TimeoutSeconds { get; set; } = 120;
|
public int TimeoutSeconds { get; set; } = 120;
|
||||||
public bool UseSoapApi { get; set; } = false; // Se usare SOAP invece di REST
|
public bool UseSoapApi { get; set; } = false; // Se usare SOAP invece di REST
|
||||||
|
/// <summary>Tipo di flusso OAuth2 da utilizzare. Default: Password (retrocompatibile).</summary>
|
||||||
|
public SalesforceGrantType GrantType { get; set; } = SalesforceGrantType.Password;
|
||||||
public string? RefreshToken { get; set; }
|
public string? RefreshToken { get; set; }
|
||||||
public string? AccessToken { get; set; }
|
public string? AccessToken { get; set; }
|
||||||
public DateTime? TokenExpiry { get; set; }
|
public DateTime? TokenExpiry { get; set; }
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ public class CredentialService : ICredentialService
|
|||||||
additionalParams["ApiVersion"] = credential.ApiVersion;
|
additionalParams["ApiVersion"] = credential.ApiVersion;
|
||||||
additionalParams["IsSandbox"] = credential.IsSandbox.ToString();
|
additionalParams["IsSandbox"] = credential.IsSandbox.ToString();
|
||||||
additionalParams["UseSoapApi"] = credential.UseSoapApi.ToString();
|
additionalParams["UseSoapApi"] = credential.UseSoapApi.ToString();
|
||||||
|
additionalParams["GrantType"] = credential.GrantType.ToString();
|
||||||
if (!string.IsNullOrEmpty(credential.RefreshToken))
|
if (!string.IsNullOrEmpty(credential.RefreshToken))
|
||||||
additionalParams["RefreshToken"] = credential.RefreshToken;
|
additionalParams["RefreshToken"] = credential.RefreshToken;
|
||||||
if (!string.IsNullOrEmpty(credential.AccessToken))
|
if (!string.IsNullOrEmpty(credential.AccessToken))
|
||||||
@@ -523,7 +524,8 @@ public class CredentialService : ICredentialService
|
|||||||
["SecurityToken"] = credential.SecurityToken,
|
["SecurityToken"] = credential.SecurityToken,
|
||||||
["ApiVersion"] = credential.ApiVersion,
|
["ApiVersion"] = credential.ApiVersion,
|
||||||
["IsSandbox"] = credential.IsSandbox.ToString(),
|
["IsSandbox"] = credential.IsSandbox.ToString(),
|
||||||
["UseSoapApi"] = credential.UseSoapApi.ToString()
|
["UseSoapApi"] = credential.UseSoapApi.ToString(),
|
||||||
|
["GrantType"] = credential.GrantType.ToString()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Aggiungi ClientId e ClientSecret se forniti
|
// Aggiungi ClientId e ClientSecret se forniti
|
||||||
@@ -793,6 +795,8 @@ public class CredentialService : ICredentialService
|
|||||||
credential.IsSandbox = sandbox;
|
credential.IsSandbox = sandbox;
|
||||||
if (additionalParams.TryGetValue("UseSoapApi", out var useSoap) && bool.TryParse(useSoap, out var soap))
|
if (additionalParams.TryGetValue("UseSoapApi", out var useSoap) && bool.TryParse(useSoap, out var soap))
|
||||||
credential.UseSoapApi = soap;
|
credential.UseSoapApi = soap;
|
||||||
|
if (additionalParams.TryGetValue("GrantType", out var grantTypeStr) && Enum.TryParse<SalesforceGrantType>(grantTypeStr, out var grantType))
|
||||||
|
credential.GrantType = grantType;
|
||||||
if (additionalParams.TryGetValue("RefreshToken", out var refreshToken))
|
if (additionalParams.TryGetValue("RefreshToken", out var refreshToken))
|
||||||
credential.RefreshToken = refreshToken;
|
credential.RefreshToken = refreshToken;
|
||||||
if (additionalParams.TryGetValue("AccessToken", out var accessToken))
|
if (additionalParams.TryGetValue("AccessToken", out var accessToken))
|
||||||
@@ -915,6 +919,8 @@ public class CredentialService : ICredentialService
|
|||||||
credential.IsSandbox = sandbox;
|
credential.IsSandbox = sandbox;
|
||||||
if (additionalParams.TryGetValue("UseSoapApi", out var useSoap) && bool.TryParse(useSoap, out var soap))
|
if (additionalParams.TryGetValue("UseSoapApi", out var useSoap) && bool.TryParse(useSoap, out var soap))
|
||||||
credential.UseSoapApi = soap;
|
credential.UseSoapApi = soap;
|
||||||
|
if (additionalParams.TryGetValue("GrantType", out var grantTypeStr) && Enum.TryParse<SalesforceGrantType>(grantTypeStr, out var grantType))
|
||||||
|
credential.GrantType = grantType;
|
||||||
if (additionalParams.TryGetValue("RefreshToken", out var refreshToken))
|
if (additionalParams.TryGetValue("RefreshToken", out var refreshToken))
|
||||||
credential.RefreshToken = refreshToken;
|
credential.RefreshToken = refreshToken;
|
||||||
if (additionalParams.TryGetValue("AccessToken", out var accessToken))
|
if (additionalParams.TryGetValue("AccessToken", out var accessToken))
|
||||||
|
|||||||
@@ -863,8 +863,22 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tokenUrl = credential.LoginUrl.TrimEnd('/') + "/services/oauth2/token";
|
var tokenUrl = credential.LoginUrl.TrimEnd('/') + "/services/oauth2/token";
|
||||||
|
List<KeyValuePair<string, string>> tokenData;
|
||||||
|
|
||||||
var tokenData = new List<KeyValuePair<string, string>>
|
if (credential.GrantType == CredentialManager.Models.SalesforceGrantType.ClientCredentials)
|
||||||
|
{
|
||||||
|
// Client Credentials flow — server-to-server, no user
|
||||||
|
tokenData = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new("grant_type", "client_credentials"),
|
||||||
|
new("client_id", credential.ClientId ?? ""),
|
||||||
|
new("client_secret", credential.ClientSecret ?? "")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Password flow (default)
|
||||||
|
tokenData = new List<KeyValuePair<string, string>>
|
||||||
{
|
{
|
||||||
new("grant_type", "password"),
|
new("grant_type", "password"),
|
||||||
new("username", credential.Username),
|
new("username", credential.Username),
|
||||||
@@ -872,14 +886,16 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
|||||||
new("client_id", credential.ClientId ?? ""),
|
new("client_id", credential.ClientId ?? ""),
|
||||||
new("client_secret", credential.ClientSecret ?? "")
|
new("client_secret", credential.ClientSecret ?? "")
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var tokenContent = new FormUrlEncodedContent(tokenData);
|
var tokenContent = new FormUrlEncodedContent(tokenData);
|
||||||
var response = await httpClient.PostAsync(tokenUrl, tokenContent);
|
var response = await httpClient.PostAsync(tokenUrl, tokenContent);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
var flowLabel = credential.GrantType == CredentialManager.Models.SalesforceGrantType.ClientCredentials
|
||||||
return (true, $"Connessione Salesforce riuscita!\n\nDettagli:\n- Login URL: {credential.LoginUrl}\n- API Version: {credential.ApiVersion}\n- Sandbox: {credential.IsSandbox}\n- Tipo Auth: OAuth2\n- Timeout: {credential.TimeoutSeconds}s");
|
? "client_credentials" : "password";
|
||||||
|
return (true, $"Connessione Salesforce riuscita!\n\nDettagli:\n- Login URL: {credential.LoginUrl}\n- API Version: {credential.ApiVersion}\n- Sandbox: {credential.IsSandbox}\n- Tipo Auth: OAuth2 ({flowLabel})\n- Timeout: {credential.TimeoutSeconds}s");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ namespace DataConnection.REST.Configuration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IgnoreSslErrors { get; set; } = false;
|
public bool IgnoreSslErrors { get; set; } = false;
|
||||||
|
|
||||||
// Add other relevant configuration properties (e.g., OAuth settings, specific headers)
|
/// <summary>
|
||||||
|
/// Salesforce OAuth2 grant type. Default: Password (retrocompatibile).
|
||||||
|
/// ClientCredentials = server-to-server, senza utente.
|
||||||
|
/// </summary>
|
||||||
|
public CredentialManager.Models.SalesforceGrantType SalesforceGrantType { get; set; }
|
||||||
|
= CredentialManager.Models.SalesforceGrantType.Password;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -286,6 +286,8 @@ public class ScheduledJobService : BackgroundService
|
|||||||
executionHistory.EndTime = DateTime.Now;
|
executionHistory.EndTime = DateTime.Now;
|
||||||
executionHistory.Status = "failed";
|
executionHistory.Status = "failed";
|
||||||
executionHistory.Message = $"Errore durante l'esecuzione automatica: {ex.Message}";
|
executionHistory.Message = $"Errore durante l'esecuzione automatica: {ex.Message}";
|
||||||
|
// Memorizza il dettaglio completo (stack trace) solo per scopi diagnostici;
|
||||||
|
// la UI in produzione ne mostrerà una versione sanitizzata senza percorsi di file.
|
||||||
executionHistory.ErrorDetails = ex.ToString();
|
executionHistory.ErrorDetails = ex.ToString();
|
||||||
await scheduleService.UpdateExecutionHistoryAsync(executionHistory);
|
await scheduleService.UpdateExecutionHistoryAsync(executionHistory);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -663,11 +663,30 @@ else
|
|||||||
} <!-- Campi specifici per Salesforce -->
|
} <!-- Campi specifici per Salesforce -->
|
||||||
@if (currentRestApiCredential.ServiceType == RestServiceType.Salesforce)
|
@if (currentRestApiCredential.ServiceType == RestServiceType.Salesforce)
|
||||||
{
|
{
|
||||||
<div class="alert alert-info">
|
<div class="row mb-3">
|
||||||
<strong>Opzioni di Autenticazione:</strong><br/>
|
<div class="col-md-12">
|
||||||
• <strong>Username/Password + Security Token:</strong> Autenticazione standard<br/>
|
<label class="form-label fw-semibold">Tipo di Autenticazione OAuth2</label>
|
||||||
• <strong>Username/Password + Client ID/Secret:</strong> Autenticazione OAuth<br/>
|
<InputSelect class="form-select" @bind-Value="currentRestApiCredential.GrantType">
|
||||||
• Il Security Token è richiesto solo se non si configura una Connected App (Client ID/Secret)
|
<option value="Password">Password Flow — Username + Password + Security Token (grant_type=password)</option>
|
||||||
|
<option value="ClientCredentials">Client Credentials — Server-to-Server, nessun utente (grant_type=client_credentials)</option>
|
||||||
|
</InputSelect>
|
||||||
|
@if (currentRestApiCredential.GrantType == CredentialManager.Models.SalesforceGrantType.ClientCredentials)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning mt-2 py-2">
|
||||||
|
<i class="fa fa-exclamation-triangle me-1"></i>
|
||||||
|
<strong>client_credentials</strong>: il <strong>Base URL</strong> deve essere il <strong>My Domain URL</strong> della tua org
|
||||||
|
(es. <code>https://myorg.my.salesforce.com</code>), <strong>non</strong> login.salesforce.com.<br/>
|
||||||
|
Richiede: Connected App con "Enable Client Credentials Flow" attivato e un Integration User assegnato.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="alert alert-info mt-2 py-2">
|
||||||
|
<i class="fa fa-info-circle me-1"></i>
|
||||||
|
<strong>password flow</strong>: Username, Password + Security Token. Client ID/Secret facoltativi (Connected App).
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -687,11 +706,15 @@ else
|
|||||||
<div class="form-text">Esempio: 59.0</div>
|
<div class="form-text">Esempio: 59.0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> <div class="mb-3">
|
</div>
|
||||||
|
@if (currentRestApiCredential.GrantType != CredentialManager.Models.SalesforceGrantType.ClientCredentials)
|
||||||
|
{
|
||||||
|
<div class="mb-3">
|
||||||
<label class="form-label">@GetFieldLabel("SecurityToken", currentRestApiCredential.ServiceType)</label>
|
<label class="form-label">@GetFieldLabel("SecurityToken", currentRestApiCredential.ServiceType)</label>
|
||||||
<InputText type="password" class="form-control" @bind-Value="currentRestApiCredential.SecurityToken" />
|
<InputText type="password" class="form-control" @bind-Value="currentRestApiCredential.SecurityToken" />
|
||||||
<div class="form-text">Token di sicurezza Salesforce (richiesto solo se non si usa OAuth o Connected App)</div>
|
<div class="form-text">Token di sicurezza Salesforce (richiesto solo se non si usa OAuth o Connected App)</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -738,8 +761,9 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- Campi comuni per autenticazione username/password -->
|
<!-- Campi comuni per autenticazione username/password -->
|
||||||
@if (currentRestApiCredential.ServiceType != RestServiceType.Generic ||
|
@if ((currentRestApiCredential.ServiceType != RestServiceType.Generic ||
|
||||||
(string.IsNullOrEmpty(currentRestApiCredential.ApiKey) && string.IsNullOrEmpty(currentRestApiCredential.AuthToken)))
|
(string.IsNullOrEmpty(currentRestApiCredential.ApiKey) && string.IsNullOrEmpty(currentRestApiCredential.AuthToken))) &&
|
||||||
|
!(currentRestApiCredential.ServiceType == RestServiceType.Salesforce && currentRestApiCredential.GrantType == CredentialManager.Models.SalesforceGrantType.ClientCredentials))
|
||||||
{ <div class="row">
|
{ <div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -1312,6 +1336,7 @@ else
|
|||||||
ApiVersion = credential.ApiVersion,
|
ApiVersion = credential.ApiVersion,
|
||||||
IsSandbox = credential.IsSandbox,
|
IsSandbox = credential.IsSandbox,
|
||||||
UseSoapApi = credential.UseSoapApi,
|
UseSoapApi = credential.UseSoapApi,
|
||||||
|
GrantType = credential.GrantType,
|
||||||
RefreshToken = credential.RefreshToken,
|
RefreshToken = credential.RefreshToken,
|
||||||
AccessToken = credential.AccessToken,
|
AccessToken = credential.AccessToken,
|
||||||
TokenExpiry = credential.TokenExpiry
|
TokenExpiry = credential.TokenExpiry
|
||||||
@@ -1533,7 +1558,8 @@ else
|
|||||||
ClientSecret = currentRestApiCredential.ClientSecret,
|
ClientSecret = currentRestApiCredential.ClientSecret,
|
||||||
ApiVersion = currentRestApiCredential.ApiVersion,
|
ApiVersion = currentRestApiCredential.ApiVersion,
|
||||||
IsSandbox = currentRestApiCredential.IsSandbox,
|
IsSandbox = currentRestApiCredential.IsSandbox,
|
||||||
UseSoapApi = currentRestApiCredential.UseSoapApi
|
UseSoapApi = currentRestApiCredential.UseSoapApi,
|
||||||
|
GrantType = currentRestApiCredential.GrantType
|
||||||
}; // Salviamo temporaneamente la credenziale per il test
|
}; // Salviamo temporaneamente la credenziale per il test
|
||||||
await CredentialService.SaveRestApiCredentialAsync(tempCredential);
|
await CredentialService.SaveRestApiCredentialAsync(tempCredential);
|
||||||
|
|
||||||
|
|||||||
@@ -311,14 +311,21 @@ public partial class Scheduling : ComponentBase
|
|||||||
|
|
||||||
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, status, message, result.RecordsProcessed);
|
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, status, message, result.RecordsProcessed);
|
||||||
|
|
||||||
|
// Notifica l'utente (best-effort: la connessione browser potrebbe essere stata interrotta
|
||||||
|
// durante un'esecuzione lunga senza che questo invalidi il risultato già salvato).
|
||||||
|
try
|
||||||
|
{
|
||||||
if (result.IsSuccess)
|
if (result.IsSuccess)
|
||||||
{
|
|
||||||
await ShowSuccessMessage($"Schedulazione eseguita con successo! {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi.");
|
await ShowSuccessMessage($"Schedulazione eseguita con successo! {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi.");
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
await ShowErrorMessage($"Errore durante l'esecuzione: {result.ErrorMessage}");
|
await ShowErrorMessage($"Errore durante l'esecuzione: {result.ErrorMessage}");
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// La connessione Blazor è stata interrotta durante l'esecuzione: il risultato è
|
||||||
|
// già stato salvato correttamente, la notifica non può essere recapitata.
|
||||||
|
Logger.LogWarning("Notifica UI non inviata per la schedulazione {ScheduleId}: connessione browser interrotta durante l'esecuzione", scheduleId);
|
||||||
|
}
|
||||||
|
|
||||||
await LoadSchedules();
|
await LoadSchedules();
|
||||||
}
|
}
|
||||||
@@ -326,7 +333,7 @@ public partial class Scheduling : ComponentBase
|
|||||||
{
|
{
|
||||||
Logger.LogError(ex, "Errore nell'esecuzione manuale schedulazione {ScheduleId}", scheduleId);
|
Logger.LogError(ex, "Errore nell'esecuzione manuale schedulazione {ScheduleId}", scheduleId);
|
||||||
|
|
||||||
// Aggiorna lo storico in caso di eccezione
|
// Aggiorna lo storico in caso di eccezione durante l'esecuzione effettiva
|
||||||
if (executionHistory != null)
|
if (executionHistory != null)
|
||||||
{
|
{
|
||||||
executionHistory.EndTime = DateTime.Now;
|
executionHistory.EndTime = DateTime.Now;
|
||||||
@@ -337,8 +344,16 @@ public partial class Scheduling : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, "failed", $"Errore: {ex.Message}");
|
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, "failed", $"Errore: {ex.Message}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
await ShowErrorMessage("Errore nell'esecuzione: " + ex.Message);
|
await ShowErrorMessage("Errore nell'esecuzione: " + ex.Message);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Notifica UI non inviata per la schedulazione {ScheduleId}: connessione browser non disponibile", scheduleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isExecuting = false;
|
isExecuting = false;
|
||||||
|
|||||||
@@ -235,7 +235,15 @@
|
|||||||
{
|
{
|
||||||
<h6>Dettagli Errori</h6>
|
<h6>Dettagli Errori</h6>
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
|
@if (IsDevelopment)
|
||||||
|
{
|
||||||
<pre style="white-space: pre-wrap; font-size: 0.85em;">@selectedExecution.ErrorDetails</pre>
|
<pre style="white-space: pre-wrap; font-size: 0.85em;">@selectedExecution.ErrorDetails</pre>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="mb-1">@GetSanitizedErrorMessage(selectedExecution.ErrorDetails)</p>
|
||||||
|
<small class="text-muted"><i class="fas fa-info-circle"></i> Per i dettagli tecnici completi consultare i log dell'applicazione.</small>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using CredentialManager.Models;
|
using CredentialManager.Models;
|
||||||
using CredentialManager.Services;
|
using CredentialManager.Services;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
@@ -11,6 +12,29 @@ public partial class SchedulingHistory : ComponentBase
|
|||||||
[Inject] private IProfileScheduleService ScheduleService { get; set; } = null!;
|
[Inject] private IProfileScheduleService ScheduleService { get; set; } = null!;
|
||||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||||
[Inject] private ILogger<SchedulingHistory> Logger { get; set; } = null!;
|
[Inject] private ILogger<SchedulingHistory> Logger { get; set; } = null!;
|
||||||
|
[Inject] private IWebHostEnvironment WebHostEnvironment { get; set; } = null!;
|
||||||
|
|
||||||
|
protected bool IsDevelopment => WebHostEnvironment.IsDevelopment();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restituisce solo il messaggio dell'eccezione (senza stack trace) per la visualizzazione in produzione.
|
||||||
|
/// </summary>
|
||||||
|
protected static string GetSanitizedErrorMessage(string errorDetails)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(errorDetails))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
// Prende solo le righe fino al primo stack frame (riga che inizia con " at")
|
||||||
|
var lines = errorDetails.Split('\n');
|
||||||
|
var messageLines = new System.Collections.Generic.List<string>();
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (line.TrimStart().StartsWith("at ", StringComparison.Ordinal))
|
||||||
|
break;
|
||||||
|
messageLines.Add(line.TrimEnd());
|
||||||
|
}
|
||||||
|
return string.Join("\n", messageLines).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
protected List<ScheduleExecutionHistory>? executionHistory;
|
protected List<ScheduleExecutionHistory>? executionHistory;
|
||||||
protected ScheduleExecutionHistory? selectedExecution;
|
protected ScheduleExecutionHistory? selectedExecution;
|
||||||
|
|||||||
@@ -133,7 +133,9 @@ namespace Data_Coupler.Services
|
|||||||
// Per Salesforce usiamo i campi specifici ClientId e ClientSecret
|
// Per Salesforce usiamo i campi specifici ClientId e ClientSecret
|
||||||
options.ApiKey = credential.ClientId; // ClientId -> ApiKey
|
options.ApiKey = credential.ClientId; // ClientId -> ApiKey
|
||||||
options.AuthToken = credential.ClientSecret; // ClientSecret -> AuthToken
|
options.AuthToken = credential.ClientSecret; // ClientSecret -> AuthToken
|
||||||
_logger.LogInformation("Salesforce mapping - ClientId: '{ClientId}', ClientSecret: {HasSecret}, Username: '{Username}', Password: {HasPassword}",
|
options.SalesforceGrantType = credential.GrantType;
|
||||||
|
_logger.LogInformation("Salesforce mapping - GrantType: {GrantType}, ClientId: '{ClientId}', ClientSecret: {HasSecret}, Username: '{Username}', Password: {HasPassword}",
|
||||||
|
credential.GrantType,
|
||||||
credential.ClientId,
|
credential.ClientId,
|
||||||
!string.IsNullOrEmpty(credential.ClientSecret),
|
!string.IsNullOrEmpty(credential.ClientSecret),
|
||||||
credential.Username,
|
credential.Username,
|
||||||
@@ -215,7 +217,9 @@ namespace Data_Coupler.Services
|
|||||||
{
|
{
|
||||||
var httpClientFactory = _serviceProvider.GetRequiredService<IHttpClientFactory>();
|
var httpClientFactory = _serviceProvider.GetRequiredService<IHttpClientFactory>();
|
||||||
var httpClient = httpClientFactory.CreateClient();
|
var httpClient = httpClientFactory.CreateClient();
|
||||||
return new SalesforceServiceClient(httpClient, options);
|
var loggerFactory = _serviceProvider.GetService<Microsoft.Extensions.Logging.ILoggerFactory>();
|
||||||
|
var sfLogger = loggerFactory?.CreateLogger<DataConnection.REST.Implementations.SalesforceServiceClient>();
|
||||||
|
return new DataConnection.REST.Implementations.SalesforceServiceClient(httpClient, options, sfLogger);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user