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,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ADS.Helpers.Base\ADS.Helpers.Base.csproj" />
<ProjectReference Include="..\ADS.Helpers.MultiTenant\ADS.Helpers.MultiTenant.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.9" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,82 @@
using System.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
namespace ADS.Helpers.Middleware.ADSException;
using ADS.Helpers.Middleware.Http;
public sealed class AdsExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly IAdsErrorLogger _logger;
public AdsExceptionMiddleware(RequestDelegate next, IAdsErrorLogger logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (System.UnauthorizedAccessException ex)
{
await Handle(context, HttpStatusCode.Unauthorized, ex, AdsErrorKind.Controlled);
}
catch (ArgumentException ex)
{
await Handle(context, HttpStatusCode.BadRequest, ex, AdsErrorKind.Controlled);
}
catch (InvalidOperationException ex)
{
await Handle(context, HttpStatusCode.BadRequest, ex, AdsErrorKind.Controlled);
}
catch (Microsoft.Data.SqlClient.SqlException ex)
{
await Handle(context, HttpStatusCode.InternalServerError, ex, AdsErrorKind.Sql);
}
catch (Exception ex)
{
await Handle(context, HttpStatusCode.InternalServerError, ex, AdsErrorKind.Unhandled);
}
}
private async Task Handle(HttpContext ctx, HttpStatusCode code, Exception ex, AdsErrorKind kind)
{
await _logger.LogAsync(new AdsErrorEvent
{
Kind = kind,
Path = ctx.Request.Path.ToString(),
Method = ctx.Request.Method,
TraceId = ctx.TraceIdentifier,
Message = ex.Message,
Exception = ex.ToString()
});
ctx.Response.StatusCode = (int)code;
ctx.Response.ContentType = "application/json";
var payload = new ApiResponse<object>
{
StatusCode = (int)code,
StatusCodeText = code.ToString(),
ConsoleMessage = ex.Message,
ScreenMessage = new UiMessage
{
MessageType = 2, // ejemplo: 1=info,2=error,3=warn
Message = "Ocurrió un error al procesar la solicitud.",
MessageCode = 0
},
Value = null
};
await ctx.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}));
}
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json;
using Microsoft.Extensions.Options;
namespace ADS.Helpers.Middleware.ADSException;
public sealed class FileAdsErrorLogger : IAdsErrorLogger
{
private readonly string _folder;
public FileAdsErrorLogger(IOptions<LoggingOptions> options)
{
_folder = options.Value.LogFolder ?? "logs";
Directory.CreateDirectory(_folder);
}
public Task LogAsync(AdsErrorEvent evt, CancellationToken ct = default)
{
var day = DateTime.UtcNow.ToString("yyyy-MM-dd");
var path = Path.Combine(_folder, $"ads-errors-{day}.jsonl");
var line = JsonSerializer.Serialize(evt);
return File.AppendAllTextAsync(path, line + Environment.NewLine, ct);
}
}

View File

@@ -0,0 +1,24 @@
namespace ADS.Helpers.Middleware.ADSException;
public enum AdsErrorKind
{
Sql = 1,
Controlled = 2,
Unhandled = 3
}
public sealed class AdsErrorEvent
{
public AdsErrorKind Kind { get; init; }
public string? Path { get; init; }
public string? Method { get; init; }
public string? TraceId { get; init; }
public string? Message { get; init; }
public string? Exception { get; init; }
public DateTime Utc { get; init; } = DateTime.UtcNow;
}
public interface IAdsErrorLogger
{
Task LogAsync(AdsErrorEvent evt, CancellationToken ct = default);
}

View File

@@ -0,0 +1,6 @@
namespace ADS.Helpers.Middleware.ADSException;
public sealed class LoggingOptions
{
public string LogFolder { get; set; } = "logs";
}

View File

@@ -0,0 +1,34 @@
using ADS.Helpers.Middleware.ADSException;
using ADS.Helpers.Middleware.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace ADS.Helpers.Middleware.ADSExtensions;
public static class AdsApiResponseExtensions
{
public static IServiceCollection AddAdsApiResponse(
this IServiceCollection services,
IConfiguration configuration)
{
// Leer configuración desde appsettings de la API
services.Configure<LoggingOptions>(
configuration.GetSection("LoggingOptions"));
services.AddSingleton<IAdsErrorLogger, FileAdsErrorLogger>();
services.AddControllers(o =>
{
o.Filters.Add<ApiResponseWrapFilter>();
});
return services;
}
public static IApplicationBuilder UseAdsApiResponse(this IApplicationBuilder app)
{
app.UseMiddleware<AdsExceptionMiddleware>();
return app;
}
}

View File

@@ -0,0 +1,76 @@
using ADS.Helpers.Middleware.Tenant;
using ADS.Helpers.Middleware.Logging;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ADS.Helpers.MultiTenant.Abstractions;
using ADS.Helpers.MultiTenant.Resolvers;
using ADS.Helpers.MultiTenant.Context;
using ADS.Helpers.Base.Abstractions;
using ADS.Helpers.MultiTenant.Configuration;
using ADS.Helpers.MultiTenant.Stores;
using ADS.Helpers.Middleware.InternalUser;
namespace ADS.Helpers.Middleware.Extensions;
public static class AdsExtensions
{
// IServiceCollection extensions
public static IServiceCollection AddAdsLogging(this IServiceCollection services, IConfiguration configuration)
{
//services.AddLogging();
// Ejemplo si tenías esto:
services.AddSingleton<IAppLogger, DefaultAppLogger>();
return services;
}
public static IServiceCollection AddAdsMultiTenant(this IServiceCollection services, IConfiguration configuration)
{
// Aquí va lo que ya tengas para tenant: options, accessor, resolver, etc.
services.Configure<TenantOptions>(configuration.GetSection("MultiTenant"));
services.AddSingleton<ITenantContextAccessor, TenantContextAccessor>();
// services.AddSingleton<ITenantStore, InMemoryTenantStore>();// reemplazado por ConfigurationTenantStore
services.AddSingleton<ITenantStore, ConfigurationTenantStore>();
services.AddScoped<ITenantResolver, HeaderTenantResolver>();
return services;
}
public static IServiceCollection AddAdsMiddlewares(this IServiceCollection services, IConfiguration configuration)
{
// Si este método es solo un "orquestador" de otros registros:
services.AddAdsLogging(configuration);
services.AddAdsMultiTenant(configuration);
// Y aquí agregas otros (ExceptionHandling, CorrelationId, etc.)
return services;
}
// IApplicationBuilder extensions
public static IApplicationBuilder UseAdsTenant(this IApplicationBuilder app)
{
return app.UseMiddleware<TenantMiddleware>();
}
public static IApplicationBuilder UseAdsMiddlewares(this IApplicationBuilder app)
{
// Orden típico: errores/correlation/tenant/etc (según tu diseño)
app.UseAdsTenant();
return app;
}
public static IServiceCollection AddAdsInternalUser(this IServiceCollection services)
{
services.AddScoped<IInternalUserContextAccessor, InternalUserContextAccessor>();
return services;
}
public static IApplicationBuilder UseAdsInternalUser(this IApplicationBuilder app)
{
return app.UseMiddleware<InternalUserContextMiddleware>();
}
}

View File

@@ -0,0 +1,20 @@
namespace ADS.Helpers.Middleware.Http;
public sealed class ApiResponse<T>
{
public int StatusCode { get; init; }
public int TotalRows { get; init; }
public string StatusCodeText { get; init; } = string.Empty;
public string? ConsoleMessage { get; init; }
public UiMessage? ScreenMessage { get; init; }
public T? Value { get; init; }
}
public sealed class UiMessage
{
public int MessageType { get; init; }
public string Message { get; init; } = "";
public int MessageCode { get; init; }
}

View File

@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace ADS.Helpers.Middleware.Http;
public sealed class ApiResponseWrapFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
// Si es FileResult / Stream / etc., no lo toques
if (context.Result is FileResult)
{
await next();
return;
}
if (context.Result is ObjectResult obj)
{
// Si ya viene envuelto, no reenvolver
if (obj.Value is not null)
{
var t = obj.Value.GetType();
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ApiResponse<>))
{
await next();
return;
}
}
var code = obj.StatusCode ?? 200;
context.Result = new ObjectResult(new ApiResponse<object>
{
StatusCode = code,
StatusCodeText = ReasonPhrase(code),
Value = obj.Value
})
{ StatusCode = code };
await next();
return;
}
// Si no es ObjectResult (por ejemplo EmptyResult), déjalo pasar
await next();
}
private static string ReasonPhrase(int code) => code switch
{
200 => "OK",
201 => "Created",
202 => "Accepted",
204 => "NoContent",
400 => "BadRequest",
401 => "Unauthorized",
403 => "Forbidden",
404 => "NotFound",
409 => "Conflict",
422 => "UnprocessableEntity",
500 => "ServerError",
_ => "Status"
};
}

View File

@@ -0,0 +1,9 @@
namespace ADS.Helpers.Middleware.Http;
public sealed class PagedQuery
{
public string? Search { get; init; }
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 50;
public bool Pagination { get; init; } = true;
}

View File

@@ -0,0 +1,11 @@
namespace ADS.Helpers.Middleware.Http;
public sealed class RequestOf<T>
{
public T? Value { get; init; }
public string? Search { get; init; }
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 50;
public bool Pagination { get; init; } = true;
}

View File

@@ -0,0 +1,6 @@
namespace ADS.Helpers.Middleware.InternalUser;
public interface IInternalUserContextAccessor
{
InternalUserContext? Current { get; set; }
}

View File

@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace ADS.Helpers.Middleware.InternalUser;
public class InternalAuthorizeAttribute : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var allowAnonymous = context.ActionDescriptor.EndpointMetadata
.OfType<AllowAnonymousAttribute>()
.Any();
if (allowAnonymous)
return;
var accessor = context.HttpContext.RequestServices
.GetService(typeof(IInternalUserContextAccessor)) as IInternalUserContextAccessor;
if (accessor?.Current == null)
{
context.Result = new UnauthorizedResult();
}
}
}

View File

@@ -0,0 +1,8 @@
namespace ADS.Helpers.Middleware.InternalUser;
public class InternalUserContext
{
public int CveUser { get; set; }
public int CveEmpresa { get; set; }
public int CveCliente { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace ADS.Helpers.Middleware.InternalUser;
public class InternalUserContextAccessor : IInternalUserContextAccessor
{
public InternalUserContext? Current { get; set; }
}

View File

@@ -0,0 +1,67 @@
using System.Security.Cryptography;
using System.Text;
using ADS.Helpers.MultiTenant.InternalRouting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
namespace ADS.Helpers.Middleware.InternalUser;
public class InternalUserContextMiddleware
{
private readonly RequestDelegate _next;
public InternalUserContextMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(
HttpContext context,
IInternalUserContextAccessor accessor,
IConfiguration configuration)
{
var cveUserHeader = context.Request.Headers[InternalHeaders.CveUser].FirstOrDefault();
var cveEmpresaHeader = context.Request.Headers[InternalHeaders.CveEmpresa].FirstOrDefault();
var cveClienteHeader = context.Request.Headers[InternalHeaders.CveCliente].FirstOrDefault();
var timestampHeader = context.Request.Headers[InternalHeaders.Timestamp].FirstOrDefault();
var signatureHeader = context.Request.Headers[InternalHeaders.User_Signature].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(cveUserHeader) &&
!string.IsNullOrWhiteSpace(cveEmpresaHeader) &&
!string.IsNullOrWhiteSpace(cveClienteHeader) &&
!string.IsNullOrWhiteSpace(timestampHeader) &&
!string.IsNullOrWhiteSpace(signatureHeader))
{
var secret = configuration["InternalRouting:SharedSecret"];
if (string.IsNullOrWhiteSpace(secret))
throw new InvalidOperationException("InternalRouting:SharedSecret no está configurado.");
var raw = $"{cveUserHeader}|{cveEmpresaHeader}|{cveClienteHeader}|{timestampHeader}";
var expectedSignature = ComputeHmacSha256(raw, secret);
if (signatureHeader == expectedSignature)
{
accessor.Current = new InternalUserContext
{
CveUser = int.Parse(cveUserHeader),
CveEmpresa = int.Parse(cveEmpresaHeader),
CveCliente = int.Parse(cveClienteHeader)
};
}
}
await _next(context);
}
private static string ComputeHmacSha256(string value, string secret)
{
var key = Encoding.UTF8.GetBytes(secret);
var bytes = Encoding.UTF8.GetBytes(value);
using var hmac = new HMACSHA256(key);
var hash = hmac.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
}

View File

@@ -0,0 +1,24 @@
using System;
using ADS.Helpers.Base.Abstractions;
using Microsoft.Extensions.Logging;
namespace ADS.Helpers.Middleware.Logging;
public sealed class DefaultAppLogger : IAppLogger
{
private readonly ILogger<DefaultAppLogger> _logger;
public DefaultAppLogger(ILogger<DefaultAppLogger> logger)
{
_logger = logger;
}
public void Info(string source, string code, string message, object? data = null)
=> _logger.LogInformation("{Source} {Code} {Message} {@Data}", source, code, message, data);
public void Warn(string source, string code, string message, object? data = null)
=> _logger.LogWarning("{Source} {Code} {Message} {@Data}", source, code, message, data);
public void Error(string source, string code, string message, Exception? ex = null, object? data = null)
=> _logger.LogError(ex, "{Source} {Code} {Message} {@Data}", source, code, message, data);
}

View File

@@ -0,0 +1,57 @@
using System.Threading.Tasks;
using ADS.Helpers.Base.Abstractions;
using ADS.Helpers.MultiTenant.Abstractions;
using ADS.Helpers.MultiTenant.Context;
using Microsoft.AspNetCore.Http;
namespace ADS.Helpers.Middleware.Tenant;
public sealed class TenantMiddleware
{
private readonly RequestDelegate _next;
public TenantMiddleware(RequestDelegate next)
{
_next = next;
}
// Scoped dependencies se resuelven aquí (por request)
public async Task InvokeAsync(
HttpContext context,
ITenantResolver tenantResolver,
ITenantStore tenantStore,
ITenantContextAccessor tenantContextAccessor,
IAppLogger log)
{
var tenantKey = tenantResolver.Resolve(context);
if (string.IsNullOrWhiteSpace(tenantKey))
{
log.Warn("ADS.MultiTenant", "TENANT_NOT_RESOLVED", "Tenant key could not be resolved.");
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("Tenant not resolved.");
return;
}
var tenant = await tenantStore.GetAsync(tenantKey);
if (tenant is null)
{
log.Warn("ADS.MultiTenant", "TENANT_NOT_FOUND", $"Tenant '{tenantKey}' not found.");
context.Response.StatusCode = StatusCodes.Status404NotFound;
await context.Response.WriteAsync("Tenant not found.");
return;
}
tenantContextAccessor.SetCurrent(tenant);
try
{
await _next(context);
}
finally
{
tenantContextAccessor.SetCurrent(null);
}
}
}