chore: initial commit - baseline before redesign
This commit is contained in:
133
CertReports.Syncfusion/Helpers/CryptoHelper.cs
Normal file
133
CertReports.Syncfusion/Helpers/CryptoHelper.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace CertReports.Syncfusion.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper per decodifica ISIN cifrato.
|
||||
/// Compatibile con SQL Server ENCRYPTBYPASSPHRASE (versione 1 = TripleDES, versione 2 = AES).
|
||||
/// Portato da CommonClass.DecryptCombined.
|
||||
/// </summary>
|
||||
public class CryptoHelper
|
||||
{
|
||||
private readonly string _passphrase;
|
||||
|
||||
public CryptoHelper(IConfiguration config)
|
||||
{
|
||||
_passphrase = config["CryptoSettings:Passphrase"]
|
||||
?? throw new InvalidOperationException("CryptoSettings:Passphrase non configurata.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodifica una stringa esadecimale cifrata con SQL Server ENCRYPTBYPASSPHRASE.
|
||||
/// Formato input: "0x0200000047..." (stringa hex con prefisso 0x).
|
||||
/// </summary>
|
||||
public string DecryptIsin(string fromSql)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fromSql))
|
||||
return string.Empty;
|
||||
|
||||
return DecryptCombined(fromSql, _passphrase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementazione completa della decodifica ENCRYPTBYPASSPHRASE di SQL Server.
|
||||
/// Supporta versione 1 (TripleDES/SHA1) e versione 2 (AES/SHA256).
|
||||
/// </summary>
|
||||
private static string DecryptCombined(string fromSql, string password)
|
||||
{
|
||||
// Password codificata come UTF16-LE (come fa SQL Server internamente)
|
||||
byte[] passwordBytes = Encoding.Unicode.GetBytes(password);
|
||||
|
||||
// Rimuove il prefisso "0x"
|
||||
if (fromSql.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
fromSql = fromSql[2..];
|
||||
|
||||
// I primi 4 byte (8 caratteri hex) indicano la versione
|
||||
int version = BitConverter.ToInt32(HexToBytes(fromSql[..8]), 0);
|
||||
|
||||
byte[] encrypted;
|
||||
HashAlgorithm hashAlgo;
|
||||
SymmetricAlgorithm cryptoAlgo;
|
||||
int keySize;
|
||||
|
||||
if (version == 1)
|
||||
{
|
||||
// Versione 1: TripleDES + SHA1
|
||||
keySize = 16;
|
||||
hashAlgo = SHA1.Create();
|
||||
cryptoAlgo = TripleDES.Create();
|
||||
cryptoAlgo.IV = HexToBytes(fromSql.Substring(8, 16)); // 8 byte IV = 16 hex chars
|
||||
encrypted = HexToBytes(fromSql[24..]); // Resto = dati cifrati
|
||||
}
|
||||
else if (version == 2)
|
||||
{
|
||||
// Versione 2: AES + SHA256
|
||||
keySize = 32;
|
||||
hashAlgo = SHA256.Create();
|
||||
cryptoAlgo = Aes.Create();
|
||||
cryptoAlgo.IV = HexToBytes(fromSql.Substring(8, 32)); // 16 byte IV = 32 hex chars
|
||||
encrypted = HexToBytes(fromSql[40..]); // Resto = dati cifrati
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new CryptographicException($"Versione di cifratura non supportata: {version}");
|
||||
}
|
||||
|
||||
cryptoAlgo.Padding = PaddingMode.PKCS7;
|
||||
cryptoAlgo.Mode = CipherMode.CBC;
|
||||
|
||||
// Genera la chiave dall'hash della password
|
||||
hashAlgo.TransformFinalBlock(passwordBytes, 0, passwordBytes.Length);
|
||||
cryptoAlgo.Key = hashAlgo.Hash!.Take(keySize).ToArray();
|
||||
|
||||
// Decrittazione
|
||||
byte[] decrypted = cryptoAlgo.CreateDecryptor()
|
||||
.TransformFinalBlock(encrypted, 0, encrypted.Length);
|
||||
|
||||
// Validazione magic number (i primi 4 byte devono essere 0xBAADF00D)
|
||||
uint magic = BitConverter.ToUInt32(decrypted, 0);
|
||||
if (magic != 0xBAADF00D)
|
||||
{
|
||||
throw new CryptographicException(
|
||||
"Decrittazione fallita: magic number non valido. Password errata?");
|
||||
}
|
||||
|
||||
// I byte 4-5 riservati, byte 6-7 = lunghezza, byte 8+ = dati
|
||||
byte[] decryptedData = decrypted[8..];
|
||||
|
||||
// Rileva encoding: se contiene byte 0x00 è UTF-16, altrimenti UTF-8
|
||||
bool isUtf16 = Array.IndexOf(decryptedData, (byte)0) != -1;
|
||||
string result = isUtf16
|
||||
? Encoding.Unicode.GetString(decryptedData)
|
||||
: Encoding.UTF8.GetString(decryptedData);
|
||||
|
||||
// Cleanup risorse crittografiche
|
||||
hashAlgo.Dispose();
|
||||
cryptoAlgo.Dispose();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converte una stringa esadecimale in byte array.
|
||||
/// </summary>
|
||||
private static byte[] HexToBytes(string hex)
|
||||
{
|
||||
byte[] bytes = new byte[hex.Length / 2];
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Genera una stringa casuale sicura.
|
||||
/// </summary>
|
||||
public static string RandomString(int length)
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
return RandomNumberGenerator.GetString(chars, length);
|
||||
}
|
||||
}
|
||||
84
CertReports.Syncfusion/Helpers/GlobalExceptionMiddleware.cs
Normal file
84
CertReports.Syncfusion/Helpers/GlobalExceptionMiddleware.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CertReports.Syncfusion.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Middleware globale per la gestione degli errori.
|
||||
/// Cattura tutte le eccezioni non gestite e restituisce una risposta JSON coerente.
|
||||
/// </summary>
|
||||
public class GlobalExceptionMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<GlobalExceptionMiddleware> _logger;
|
||||
|
||||
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Richiesta non valida: {Message}", ex.Message);
|
||||
await WriteErrorResponse(context, HttpStatusCode.BadRequest, ex.Message);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Operazione non valida: {Message}", ex.Message);
|
||||
await WriteErrorResponse(context, HttpStatusCode.BadRequest, ex.Message);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Risorsa non trovata: {Message}", ex.Message);
|
||||
await WriteErrorResponse(context, HttpStatusCode.NotFound, "Risorsa non trovata.");
|
||||
}
|
||||
catch (TimeoutException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Timeout durante la generazione del report");
|
||||
await WriteErrorResponse(context, HttpStatusCode.GatewayTimeout,
|
||||
"Timeout nella generazione del report. Riprovare.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore non gestito: {Message}", ex.Message);
|
||||
await WriteErrorResponse(context, HttpStatusCode.InternalServerError,
|
||||
"Errore interno del server. Contattare l'assistenza.");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteErrorResponse(HttpContext context, HttpStatusCode statusCode, string message)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = (int)statusCode;
|
||||
|
||||
var errorResponse = new
|
||||
{
|
||||
status = (int)statusCode,
|
||||
error = statusCode.ToString(),
|
||||
message,
|
||||
timestamp = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
await context.Response.WriteAsync(json);
|
||||
}
|
||||
}
|
||||
|
||||
public static class GlobalExceptionMiddlewareExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app)
|
||||
{
|
||||
return app.UseMiddleware<GlobalExceptionMiddleware>();
|
||||
}
|
||||
}
|
||||
69
CertReports.Syncfusion/Helpers/HealthChecks.cs
Normal file
69
CertReports.Syncfusion/Helpers/HealthChecks.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace CertReports.Syncfusion.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Verifica connettività al database SQL Server.
|
||||
/// </summary>
|
||||
public class DatabaseHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public DatabaseHealthCheck(IConfiguration config)
|
||||
{
|
||||
_connectionString = config.GetConnectionString("CertDb") ?? "";
|
||||
}
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var conn = new SqlConnection(_connectionString);
|
||||
await conn.OpenAsync(cancellationToken);
|
||||
await using var cmd = new SqlCommand("SELECT 1", conn);
|
||||
await cmd.ExecuteScalarAsync(cancellationToken);
|
||||
return HealthCheckResult.Healthy("SQL Server raggiungibile.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy("SQL Server non raggiungibile.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica connettività al servizio chart esterno.
|
||||
/// </summary>
|
||||
public class ChartServiceHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public ChartServiceHealthCheck(IHttpClientFactory httpClientFactory, IConfiguration config)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_baseUrl = config["ChartService:BaseUrl"] ?? "";
|
||||
}
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_baseUrl))
|
||||
return HealthCheckResult.Degraded("ChartService:BaseUrl non configurato.");
|
||||
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("ChartService");
|
||||
var response = await client.GetAsync(_baseUrl, cancellationToken);
|
||||
return response.IsSuccessStatusCode
|
||||
? HealthCheckResult.Healthy("Chart service raggiungibile.")
|
||||
: HealthCheckResult.Degraded($"Chart service risponde con status {response.StatusCode}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Degraded("Chart service non raggiungibile.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
126
CertReports.Syncfusion/Helpers/PdfTheme.cs
Normal file
126
CertReports.Syncfusion/Helpers/PdfTheme.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using Syncfusion.Drawing;
|
||||
using Syncfusion.Pdf;
|
||||
using Syncfusion.Pdf.Graphics;
|
||||
using Syncfusion.Pdf.Grid;
|
||||
|
||||
namespace CertReports.Syncfusion.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Tema centralizzato per tutti i PDF generati.
|
||||
/// Modifica qui colori, font e dimensioni per aggiornare l'intero report.
|
||||
/// </summary>
|
||||
public static class PdfTheme
|
||||
{
|
||||
// ─── Colori ────────────────────────────────────────────────────────
|
||||
public static readonly Color HeaderBackground = Color.FromArgb(255, 46, 80, 144); // #2E5090
|
||||
public static readonly Color HeaderText = Color.FromArgb(255, 255, 255, 255);
|
||||
public static readonly Color AlternateRow = Color.FromArgb(255, 242, 246, 250); // #F2F6FA
|
||||
public static readonly Color BorderColor = Color.FromArgb(255, 200, 210, 220);
|
||||
public static readonly Color TextPrimary = Color.FromArgb(255, 33, 37, 41);
|
||||
public static readonly Color TextSecondary = Color.FromArgb(255, 108, 117, 125);
|
||||
public static readonly Color PositiveValue = Color.FromArgb(255, 40, 167, 69); // verde
|
||||
public static readonly Color NegativeValue = Color.FromArgb(255, 220, 53, 69); // rosso
|
||||
public static readonly Color SectionTitle = Color.FromArgb(255, 46, 80, 144);
|
||||
|
||||
// ─── Font ──────────────────────────────────────────────────────────
|
||||
private static readonly PdfStandardFont _fontRegular = new(PdfFontFamily.Helvetica, 8f);
|
||||
private static readonly PdfStandardFont _fontBold = new(PdfFontFamily.Helvetica, 8f, PdfFontStyle.Bold);
|
||||
private static readonly PdfStandardFont _fontSmall = new(PdfFontFamily.Helvetica, 6.5f);
|
||||
private static readonly PdfStandardFont _fontSmallBold = new(PdfFontFamily.Helvetica, 6.5f, PdfFontStyle.Bold);
|
||||
private static readonly PdfStandardFont _fontTitle = new(PdfFontFamily.Helvetica, 14f, PdfFontStyle.Bold);
|
||||
private static readonly PdfStandardFont _fontSectionTitle = new(PdfFontFamily.Helvetica, 10f, PdfFontStyle.Bold);
|
||||
private static readonly PdfStandardFont _fontHeader = new(PdfFontFamily.Helvetica, 7f, PdfFontStyle.Bold);
|
||||
|
||||
public static PdfFont Regular => _fontRegular;
|
||||
public static PdfFont Bold => _fontBold;
|
||||
public static PdfFont Small => _fontSmall;
|
||||
public static PdfFont SmallBold => _fontSmallBold;
|
||||
public static PdfFont Title => _fontTitle;
|
||||
public static PdfFont SectionTitleFont => _fontSectionTitle;
|
||||
public static PdfFont Header => _fontHeader;
|
||||
|
||||
// ─── Margini & Layout ──────────────────────────────────────────────
|
||||
public const float PageMargin = 40f;
|
||||
public const float RowHeight = 18f;
|
||||
public const float HeaderRowHeight = 22f;
|
||||
public const float CellPadding = 4f;
|
||||
|
||||
// ─── Brushes ───────────────────────────────────────────────────────
|
||||
public static PdfBrush HeaderBrush => new PdfSolidBrush(HeaderBackground);
|
||||
public static PdfBrush HeaderTextBrush => new PdfSolidBrush(HeaderText);
|
||||
public static PdfBrush AlternateRowBrush => new PdfSolidBrush(AlternateRow);
|
||||
public static PdfBrush TextBrush => new PdfSolidBrush(TextPrimary);
|
||||
public static PdfBrush TextSecondaryBrush => new PdfSolidBrush(TextSecondary);
|
||||
public static PdfBrush PositiveBrush => new PdfSolidBrush(PositiveValue);
|
||||
public static PdfBrush NegativeBrush => new PdfSolidBrush(NegativeValue);
|
||||
public static PdfPen BorderPen => new PdfPen(BorderColor, 0.5f);
|
||||
|
||||
// ─── Utility ───────────────────────────────────────────────────────
|
||||
public static PdfBrush ValueBrush(decimal? value)
|
||||
{
|
||||
if (value == null) return TextBrush;
|
||||
return value >= 0 ? PositiveBrush : NegativeBrush;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crea una pagina A4 con margini standard
|
||||
/// </summary>
|
||||
public static PdfPage AddA4Page(PdfDocument doc, PdfPageOrientation orientation = PdfPageOrientation.Portrait)
|
||||
{
|
||||
doc.PageSettings.Size = PdfPageSize.A4;
|
||||
doc.PageSettings.Orientation = orientation;
|
||||
doc.PageSettings.Margins.All = PageMargin;
|
||||
return doc.Pages.Add();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatta un decimale come percentuale
|
||||
/// </summary>
|
||||
public static string FormatPercent(decimal? value, int decimals = 2)
|
||||
{
|
||||
if (value == null) return "-";
|
||||
return value.Value.ToString($"N{decimals}") + " %";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatta un decimale come numero
|
||||
/// </summary>
|
||||
public static string FormatNumber(decimal? value, int decimals = 2)
|
||||
{
|
||||
if (value == null) return "-";
|
||||
return value.Value.ToString($"N{decimals}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formatta una data in formato italiano
|
||||
/// </summary>
|
||||
public static string FormatDate(DateTime? date)
|
||||
{
|
||||
return date?.ToString("dd/MM/yyyy") ?? "-";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applica bordi sottili a tutte le celle di una PdfGrid
|
||||
/// </summary>
|
||||
public static void ApplyThinBorders(PdfGrid grid, float thickness = 0.25f)
|
||||
{
|
||||
var pen = new PdfPen(BorderColor, thickness);
|
||||
var borders = new PdfBorders
|
||||
{
|
||||
Top = pen,
|
||||
Bottom = pen,
|
||||
Left = pen,
|
||||
Right = pen
|
||||
};
|
||||
|
||||
// Header
|
||||
for (int r = 0; r < grid.Headers.Count; r++)
|
||||
for (int c = 0; c < grid.Headers[r].Cells.Count; c++)
|
||||
grid.Headers[r].Cells[c].Style.Borders = borders;
|
||||
|
||||
// Righe dati
|
||||
for (int r = 0; r < grid.Rows.Count; r++)
|
||||
for (int c = 0; c < grid.Rows[r].Cells.Count; c++)
|
||||
grid.Rows[r].Cells[c].Style.Borders = borders;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user