chore: initial commit - baseline before redesign

This commit is contained in:
2026-03-20 12:03:38 +01:00
commit 85ee66750a
31 changed files with 3426 additions and 0 deletions

View 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);
}
}

View 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>();
}
}

View 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);
}
}
}

View 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;
}
}