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,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.72" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
<PackageReference Include="Npgsql" Version="10.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.3" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Helpers\ADS.Helpers.Base\ADS.Helpers.Base.csproj" />
<ProjectReference Include="..\..\Helpers\ADS.Helpers.Middleware\ADS.Helpers.Middleware.csproj" />
<ProjectReference Include="..\..\Helpers\ADS.Helpers.MultiTenant\ADS.Helpers.MultiTenant.csproj" />
<ProjectReference Include="..\..\Helpers\ADS.Helpers.NotifyCenter\ADS.Helpers.NotifyCenter.csproj" />
<ProjectReference Include="..\..\Helpers\ADS.Helpers.Localization\ADS.Helpers.Localization.csproj" />
<ProjectReference Include="..\..\Helpers\ADS.Helpers.Auth\ADS.Helpers.Auth.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
@ADS.Host_HostAddress = http://localhost:5030
GET {{ADS.Host_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,142 @@
using ADS.Helpers.Auth.Abstractions;
using ADS.Helpers.MultiTenant.Abstractions;
using ADS.Helpers.MultiTenant.Context;
using Dapper;
using Microsoft.Data.SqlClient;
using Npgsql;
using System.Data;
using System.Text;
using System.Security.Cryptography;
namespace ADS.Host.Auth;
public class HostAdsUserCredentialValidator : IAdsUserCredentialValidator
{
private readonly ITenantContextAccessor _tenantContextAccessor;
public HostAdsUserCredentialValidator(ITenantContextAccessor tenantContextAccessor)
{
_tenantContextAccessor = tenantContextAccessor;
}
public async Task<AdsUserValidationResult> ValidateAsync(
string userName,
string password,
string? tenantId,
string? scope,
CancellationToken ct = default)
{
var tenant = _tenantContextAccessor.Current;
if (tenant == null)
{
return new AdsUserValidationResult
{
IsValid = false,
Error = "TenantContext no está cargado."
};
}
if (tenant.Database == null ||
string.IsNullOrWhiteSpace(tenant.Database.ConnectionString))
{
return new AdsUserValidationResult
{
IsValid = false,
Error = "El tenant no tiene conexión configurada."
};
}
using var conn = CreateConnection(
tenant.Database.Provider,
tenant.Database.ConnectionString);
var result = await conn.QueryFirstOrDefaultAsync<UsuarioLoginDbResult>(
"dbo.PA_Sis_UsuariosGet",
new
{
CveUsuario = -1,
CveRol = -1,
CveUsuarioControlado = -1,
Usuario = userName,
Contraseña = EncriptaDES(password),
EsLogIn = true,
CvePlataforma = -1,
CveEmpresa = -1,
IP = "",
MAC = "",
GrupoEmpresa = "",
Estatus = -1
},
commandType: CommandType.StoredProcedure
);
if (result == null || result.CveUsuario <= 0)
{
return new AdsUserValidationResult
{
IsValid = false,
Error = "Usuario o contraseña inválidos."
};
}
return new AdsUserValidationResult
{
IsValid = true,
UserName=userName,
CveUser = result.CveUsuario,
CveRole = result.CveRol,
CveEmpresa = result.CveEmpresa,
CveCliente = result.CveCliente,
TenantId = tenant.TenantId
};
}
private static string EncriptaDES(string texto)
{
if (string.IsNullOrEmpty(texto))
return "";
byte[] key = Encoding.UTF8.GetBytes("$Ap7+5&z");
byte[] iv = new byte[]
{
0x12, 0x34, 0x56, 0x78,
0x90, 0xAB, 0xCD, 0xEF
};
using var des = DES.Create();
des.Key = key;
des.IV = iv;
des.Mode = CipherMode.CBC;
des.Padding = PaddingMode.PKCS7;
byte[] input = Encoding.UTF8.GetBytes(texto);
using var ms = new MemoryStream();
using var cs = new CryptoStream(ms, des.CreateEncryptor(), CryptoStreamMode.Write);
cs.Write(input, 0, input.Length);
cs.FlushFinalBlock();
return Convert.ToBase64String(ms.ToArray());
}
private static IDbConnection CreateConnection(string provider, string connectionString)
{
return provider?.ToLowerInvariant() switch
{
"sqlserver" => new SqlConnection(connectionString),
"mssql" => new SqlConnection(connectionString),
"postgres" => new NpgsqlConnection(connectionString),
"postgresql" => new NpgsqlConnection(connectionString),
_ => throw new NotSupportedException($"Proveedor de base de datos no soportado: {provider}")
};
}
private class UsuarioLoginDbResult
{
public int CveUsuario { get; set; }
public int CveRol { get; set; }
public int CveEmpresa { get; set; }
public int CveCliente { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using ADS.Helpers.Auth.Abstractions;
namespace ADS.Host.Auth;
public sealed class StubAdsClientCredentialValidator : IAdsClientCredentialValidator
{
public Task<AdsClientValidationResult> ValidateAsync(
string clientId,
string clientSecret,
string? tenantId,
string? scope,
CancellationToken ct = default)
{
return Task.FromResult(new AdsClientValidationResult
{
IsValid = false,
ClientId = clientId,
TenantId = tenantId,
Error = "IAdsClientCredentialValidator not configured in ADS.Host."
});
}
}

View File

@@ -0,0 +1,20 @@
using ADS.Helpers.Auth.Abstractions;
namespace ADS.Host.Auth;
public sealed class StubAdsUserCredentialValidator : IAdsUserCredentialValidator
{
public Task<AdsUserValidationResult> ValidateAsync(
string userName,
string password,
string? tenantId,
string? scope,
CancellationToken ct = default)
{
return Task.FromResult(new AdsUserValidationResult
{
IsValid = false,
Error = "IAdsUserCredentialValidator not configured in ADS.Host."
});
}
}

View File

@@ -0,0 +1,29 @@
using ADS.Helpers.Auth.Abstractions;
using ADS.Helpers.Auth.Token;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("auth")]
public class AuthController : ControllerBase
{
private readonly IAdsTokenService _tokenService;
public AuthController(
IAdsTokenService tokenService )
{
_tokenService = tokenService;
}
[HttpPost("token")]
[AllowAnonymous]
public async Task<IActionResult> Token(
[FromBody] AdsTokenRequest request,
CancellationToken ct)
{
var token = await _tokenService.IssueAsync(request,ct);
return Ok(token);
}
}

View File

@@ -0,0 +1,36 @@
using ADS.Helpers.Auth.Abstractions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ADS.Host.Controllers;
[ApiController]
[Route("api/[controller]")]
public sealed class WhoAmIController : ControllerBase
{
private readonly IAdsPrincipalAccessor _principal;
public WhoAmIController(IAdsPrincipalAccessor principal)
{
_principal = principal;
}
[HttpGet]
[Authorize]
public IActionResult Get()
{
var me = _principal.Current;
return Ok(new
{
me.CveUser,
me.TenantId,
me.UserName,
me.CveRole,
me.CveEmpresa,
me.CveCliente,
me.CveModulos,
me.Permissions
});
}
}

125
Apis/ADS.Host/Program.cs Normal file
View File

@@ -0,0 +1,125 @@
using ADS.Helpers.Auth.Abstractions;
using ADS.Helpers.Auth.Extensions;
using ADS.Helpers.Middleware.Extensions;
using ADS.Helpers.MultiTenant.InternalRouting;
using ADS.Host.Auth;
using Yarp.ReverseProxy.Transforms;
using ADS.Helpers.Auth.Claims;
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services .AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddTransforms(builderContext =>
{
builderContext.AddRequestTransform(async transformContext =>
{
var http = transformContext.HttpContext;
// Solo para rutas /gps (por si luego agregas más clusters)
if (!http.Request.Path.StartsWithSegments("/gps"))
return;
var tenantAccessor = http.RequestServices.GetRequiredService<ADS.Helpers.MultiTenant.Context.ITenantContextAccessor>();
var writer = http.RequestServices.GetRequiredService<ADS.Helpers.MultiTenant.InternalRouting.IInternalTenantDbHeaderWriter>();
var tenant = tenantAccessor.Current;
if (tenant is null)
return; // o lanza 401/400 si quieres forzarlo
var conn = tenant.Database.ConnectionString;
if (string.IsNullOrWhiteSpace(conn))
return; // igual: mejor forzar error si lo deseas
// Agregar headers internos firmados a la request proxyeada
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
writer.AddInternalHeaders(headers, tenant);
{
var cveUser = http.User.FindFirst(AdsClaimTypes.CveUser)?.Value;
var cveEmpresa = http.User.FindFirst(AdsClaimTypes.CveEmpresa)?.Value;
var cveCliente = http.User.FindFirst(AdsClaimTypes.CveCliente)?.Value;
if (!string.IsNullOrWhiteSpace(cveUser) &&
!string.IsNullOrWhiteSpace(cveEmpresa) &&
!string.IsNullOrWhiteSpace(cveCliente))
{
var tsUser = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var secret = builderContext.Services
.GetRequiredService<IConfiguration>()["InternalRouting:SharedSecret"];
var raw = $"{cveUser}|{cveEmpresa}|{cveCliente}|{tsUser}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret!));
var signature = Convert.ToBase64String(
hmac.ComputeHash(Encoding.UTF8.GetBytes(raw)));
headers[InternalHeaders.CveUser] = cveUser;
headers[InternalHeaders.CveEmpresa] = cveEmpresa;
headers[InternalHeaders.CveCliente] = cveCliente;
headers[InternalHeaders.User_Timestamp] = tsUser;
headers[InternalHeaders.User_Signature] = signature;
}
}
foreach (var kv in headers)
{
transformContext.ProxyRequest.Headers.Remove(kv.Key);
transformContext.ProxyRequest.Headers.TryAddWithoutValidation(kv.Key, kv.Value);
}
await ValueTask.CompletedTask;
});
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Auth
builder.Services.AddAdsAuthentication(builder.Configuration);
builder.Services.AddAdsAuthorization();
// Tenant + Middlewares (ajustar a tus extensiones reales)
builder.Services.AddAdsMiddlewares(builder.Configuration);
builder.Services.AddAdsInternalUser();
// ✅ InternalRouting: Host solo necesita el writer (para agregar X-Internal-*)
builder.Services.AddInternalTenantDbHeaderWriter(builder.Configuration);
builder.Services.AddScoped<IAdsUserCredentialValidator, HostAdsUserCredentialValidator>();
builder.Services.AddScoped<IAdsClientCredentialValidator, StubAdsClientCredentialValidator>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
}
app.UseSwagger();
app.UseSwaggerUI();
// ✅ Muy importante: limpiar headers internos que vengan del cliente
app.Use((ctx, next) =>
{
ctx.Request.Headers.Remove(InternalHeaders.Tenant);
ctx.Request.Headers.Remove(InternalHeaders.Conn);
ctx.Request.Headers.Remove(InternalHeaders.Timestamp);
ctx.Request.Headers.Remove(InternalHeaders.Signature);
ctx.Request.Headers.Remove(InternalHeaders.CveUser);
ctx.Request.Headers.Remove(InternalHeaders.CveEmpresa);
ctx.Request.Headers.Remove(InternalHeaders.CveCliente);
ctx.Request.Headers.Remove(InternalHeaders.User_Timestamp);
ctx.Request.Headers.Remove(InternalHeaders.User_Signature);
return next();
});
app.UseAuthentication();
app.UseAuthorization();
app.UseAdsMiddlewares();
app.MapControllers();
app.MapReverseProxy();
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7000;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,62 @@
{
"MultiTenant": {
"HeaderName": "X-Tenant",
"DefaultTenantId": "demo",
"RequireTenant": false,
"BypassPaths": ["/swagger", "/health"]
},
"Tenants": {
"demo": {
"TenantId": "demo",
"EmpresaId": 172,
"ClienteId": 1,
"Culture": "es-MX",
"Theme": "default",
"Database": {
"Provider": "SqlServer",
"ConnectionString": "Data Source=(local);Initial Catalog=GPS;User Id=GPSUsr;Password=Productivo2022#;pooling=True; Min Pool Size=10;Application Name=APIDemo"
},
"CustomSettings": {
"GpsVendor": "Concox"
}
}
},
"Jwt": {
"Issuer": "ADS",
"Audience": "ADS_APIS",
"SigningKey": "SUPER_JCau127*953SECRET_KEY_CHANGE_THIS",
"ValidateLifetime": true,
"ClockSkewMinutes": 5
},
"ReverseProxy": {
"Routes": {
"gpsRoute": {
"ClusterId": "gpsCluster",
"Match": {
"Path": "/gps/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/gps" }
],
"Metadata": {
"RequireInternalTenant": "true"
}
}
},
"Clusters": {
"gpsCluster": {
"Destinations": {
"gps": {
"Address": "http://localhost:5023/"
}
}
}
}
},
"InternalRouting": {
"SharedSecret": "MISMO_SECRETO_DEL_HOST",
"MaxSkewSeconds": 120
}
}