First Commit

This commit is contained in:
Jorge Luis caudillo esparza
2026-05-09 16:36:57 -07:00
parent 96ad4c0d3e
commit d0c284d9c0
131 changed files with 4009 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Builder;
namespace ADS.Helpers.MultiTenant.InternalRouting;
public static class ApplicationBuilderExtensions
{
// MICRO: valida headers internos antes de controllers
public static IApplicationBuilder UseInternalTenantDb(this IApplicationBuilder app)
=> app.UseMiddleware<InternalTenantDbMiddleware>();
}

View File

@@ -0,0 +1,24 @@
namespace ADS.Helpers.MultiTenant.InternalRouting;
public static class InternalHeaders
{
public const string Tenant = "X-Internal-Tenant";
public const string Conn = "X-Internal-Conn";
public const string Timestamp = "X-Internal-Ts";
public const string Signature = "X-Internal-Sig";
// NUEVOS (lo que faltaba para no “inventar” en GPS)
public const string EmpresaId = "X-Internal-EmpresaId";
public const string ClienteId = "X-Internal-ClienteId";
// Opcionales
public const string Culture = "X-Internal-Culture";
public const string Theme = "X-Internal-Theme";
public const string DbProvider= "X-Internal-DbProvider";
public const string CveUser = "X-Internal-CveUser";
public const string CveEmpresa = "X-Internal-CveEmpresa";
public const string CveCliente = "X-Internal-CveCliente";
public const string User_Timestamp = "X-Internal-User-Timestamp";
public const string User_Signature = "X-Internal-User-Signature";
}

View File

@@ -0,0 +1,21 @@
namespace ADS.Helpers.MultiTenant.InternalRouting;
public sealed class InternalRoutingOptions
{
public string SharedSecret { get; set; } = default!;
public int MaxSkewSeconds { get; set; } = 120;
public string HeaderTenant { get; set; } = InternalHeaders.Tenant;
public string HeaderConn { get; set; } = InternalHeaders.Conn;
public string HeaderTs { get; set; } = InternalHeaders.Timestamp;
public string HeaderSig { get; set; } = InternalHeaders.Signature;
// NUEVOS
public string HeaderEmpresaId { get; set; } = InternalHeaders.EmpresaId;
public string HeaderClienteId { get; set; } = InternalHeaders.ClienteId;
// Opcionales
public string HeaderCulture { get; set; } = InternalHeaders.Culture;
public string HeaderTheme { get; set; } = InternalHeaders.Theme;
public string HeaderDbProvider{ get; set; } = InternalHeaders.DbProvider;
}

View File

@@ -0,0 +1,43 @@
using System.Security.Cryptography;
using System.Text;
namespace ADS.Helpers.MultiTenant.InternalRouting;
public static class InternalSignature
{
public static string Compute(
string secret,
string tenantId,
string conn,
int empresaId,
int clienteId,
string provider,
string culture,
string theme,
long tsUnix)
{
var payload = $"{tenantId}\n{conn}\n{empresaId}\n{clienteId}\n{provider}\n{culture}\n{theme}\n{tsUnix}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
return Convert.ToHexString(hash); // HEX uppercase
}
// Backward compatible (por si algún host viejo sigue firmando con el formato anterior)
public static string Compute(string secret, string tenantId, string conn, long tsUnix)
=> Compute(secret, tenantId, conn, 0, 0, "SqlServer", "es-MX", "", tsUnix);
public static bool FixedTimeEqualsHex(string expectedHex, string providedHex)
{
if (expectedHex.Length != providedHex.Length) return false;
return CryptographicOperations.FixedTimeEquals(
Encoding.ASCII.GetBytes(expectedHex),
Encoding.ASCII.GetBytes(providedHex));
}
public static bool IsTimestampValid(long tsUnix, int maxSkewSeconds)
{
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var diff = Math.Abs(now - tsUnix);
return diff <= maxSkewSeconds;
}
}

View File

@@ -0,0 +1,46 @@
using ADS.Helpers.MultiTenant.Context;
using Microsoft.Extensions.Options;
namespace ADS.Helpers.MultiTenant.InternalRouting;
public interface IInternalTenantDbHeaderWriter
{
long AddInternalHeaders(IDictionary<string, string> headers, TenantContext tenant);
}
public sealed class InternalTenantDbHeaderWriter : IInternalTenantDbHeaderWriter
{
private readonly InternalRoutingOptions _opt;
public InternalTenantDbHeaderWriter(IOptions<InternalRoutingOptions> opt) => _opt = opt.Value;
public long AddInternalHeaders(IDictionary<string, string> headers, TenantContext tenant)
{
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var tenantId = tenant.TenantId ?? "";
var conn = tenant.Database?.ConnectionString ?? "";
var empresaId = tenant.EmpresaId;
var clienteId = tenant.ClienteId;
var provider = tenant.Database?.Provider ?? "SqlServer";
var culture = tenant.Culture ?? "es-MX";
var theme = tenant.Theme ?? "";
var sig = InternalSignature.Compute(_opt.SharedSecret, tenantId, conn, empresaId, clienteId, provider, culture, theme, ts);
headers[_opt.HeaderTenant] = tenantId;
headers[_opt.HeaderConn] = conn;
headers[_opt.HeaderEmpresaId] = empresaId.ToString();
headers[_opt.HeaderClienteId] = clienteId.ToString();
headers[_opt.HeaderDbProvider] = provider;
headers[_opt.HeaderCulture] = culture;
headers[_opt.HeaderTheme] = theme;
headers[_opt.HeaderTs] = ts.ToString();
headers[_opt.HeaderSig] = sig;
return ts;
}
}

View File

@@ -0,0 +1,7 @@
namespace ADS.Helpers.MultiTenant.InternalRouting;
public static class InternalTenantDbHeaders
{
public const string ItemTenantId = "Internal.TenantId";
public const string ItemConn = "Internal.Conn";
}

View File

@@ -0,0 +1,134 @@
using ADS.Helpers.MultiTenant.Context;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace ADS.Helpers.MultiTenant.InternalRouting;
public sealed class InternalTenantDbMiddleware
{
private readonly RequestDelegate _next;
private readonly InternalRoutingOptions _opt;
private readonly ITenantContextAccessor _tenantAccessor;
public InternalTenantDbMiddleware(
RequestDelegate next,
IOptions<InternalRoutingOptions> opt,
ITenantContextAccessor tenantAccessor)
{
_next = next;
_opt = opt.Value;
_tenantAccessor = tenantAccessor;
}
public async Task InvokeAsync(HttpContext ctx)
{
if (!TryRead(ctx,
out var tenantId,
out var conn,
out var empresaId,
out var clienteId,
out var provider,
out var culture,
out var theme,
out var ts,
out var sig))
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
if (!InternalSignature.IsTimestampValid(ts, _opt.MaxSkewSeconds))
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
// Firma NUEVA (con empresa/cliente/etc)
var expected = InternalSignature.Compute(_opt.SharedSecret, tenantId, conn, empresaId, clienteId, provider, culture, theme, ts);
// Backward compatible: si el Host todavía firma “viejo”, cae al compute viejo
if (!InternalSignature.FixedTimeEqualsHex(expected, sig))
{
var expectedOld = InternalSignature.Compute(_opt.SharedSecret, tenantId, conn, ts);
if (!InternalSignature.FixedTimeEqualsHex(expectedOld, sig))
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
}
// backward compatible items
ctx.Items[InternalTenantDbHeaders.ItemTenantId] = tenantId;
ctx.Items[InternalTenantDbHeaders.ItemConn] = conn;
_tenantAccessor.SetCurrent(new TenantContext
{
TenantId = tenantId,
EmpresaId = empresaId,
ClienteId = clienteId,
Culture = culture,
Theme = theme,
Database = new DatabaseConfig
{
Provider = provider,
ConnectionString = conn
}
});
try
{
await _next(ctx);
}
finally
{
_tenantAccessor.SetCurrent(null);
}
}
private bool TryRead(
HttpContext ctx,
out string tenantId,
out string conn,
out int empresaId,
out int clienteId,
out string provider,
out string culture,
out string theme,
out long ts,
out string sig)
{
tenantId = conn = sig = "";
empresaId = 0;
clienteId = 0;
provider = "SqlServer";
culture = "es-MX";
theme = "";
ts = 0;
if (!ctx.Request.Headers.TryGetValue(_opt.HeaderTenant, out var t)) return false;
if (!ctx.Request.Headers.TryGetValue(_opt.HeaderConn, out var c)) return false;
if (!ctx.Request.Headers.TryGetValue(_opt.HeaderTs, out var tsStr)) return false;
if (!ctx.Request.Headers.TryGetValue(_opt.HeaderSig, out var s)) return false;
tenantId = t.ToString();
conn = c.ToString();
sig = s.ToString();
// NUEVOS obligatorios para Sicrep (si no vienen, dejamos 0 y el SP puede trabajar con -1)
if (ctx.Request.Headers.TryGetValue(_opt.HeaderEmpresaId, out var eStr))
int.TryParse(eStr.ToString(), out empresaId);
if (ctx.Request.Headers.TryGetValue(_opt.HeaderClienteId, out var clStr))
int.TryParse(clStr.ToString(), out clienteId);
// Opcionales
if (ctx.Request.Headers.TryGetValue(_opt.HeaderDbProvider, out var p)) provider = p.ToString();
if (ctx.Request.Headers.TryGetValue(_opt.HeaderCulture, out var cul)) culture = cul.ToString();
if (ctx.Request.Headers.TryGetValue(_opt.HeaderTheme, out var th)) theme = th.ToString();
return !string.IsNullOrWhiteSpace(tenantId)
&& !string.IsNullOrWhiteSpace(conn)
&& long.TryParse(tsStr.ToString(), out ts)
&& !string.IsNullOrWhiteSpace(sig);
}
}

View File

@@ -0,0 +1,29 @@
using ADS.Helpers.MultiTenant.Context;
using ADS.Helpers.MultiTenant.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace ADS.Helpers.MultiTenant.InternalRouting;
public static class ServiceCollectionExtensions
{
// MICRO: registra middleware options + HttpContextAccessor + DB factory
public static IServiceCollection AddInternalTenantDb(this IServiceCollection services, IConfiguration cfg)
{
services.AddHttpContextAccessor();
services.Configure<InternalRoutingOptions>(cfg.GetSection("InternalRouting"));
services.AddSingleton<IDbConnectionFactory, TenantDbConnectionFactory>();
services.AddSingleton<ITenantContextAccessor, TenantContextAccessor>();
services.AddSingleton<IDbConnectionFactory, TenantDbConnectionFactory>();
return services;
}
// HOST: registra solo el writer (para agregar headers internos)
public static IServiceCollection AddInternalTenantDbHeaderWriter(this IServiceCollection services, IConfiguration cfg)
{
services.Configure<InternalRoutingOptions>(cfg.GetSection("InternalRouting"));
services.AddSingleton<IInternalTenantDbHeaderWriter, InternalTenantDbHeaderWriter>();
return services;
}
}