First Commit
This commit is contained in:
27
Apis/ADS.Host/ADS.Host.csproj
Normal file
27
Apis/ADS.Host/ADS.Host.csproj
Normal 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>
|
||||
6
Apis/ADS.Host/ADS.Host.http
Normal file
6
Apis/ADS.Host/ADS.Host.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@ADS.Host_HostAddress = http://localhost:5030
|
||||
|
||||
GET {{ADS.Host_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
142
Apis/ADS.Host/Auth/HostAdsUserCredentialValidator .cs
Normal file
142
Apis/ADS.Host/Auth/HostAdsUserCredentialValidator .cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
22
Apis/ADS.Host/Auth/StubAdsClientCredentialValidator.cs
Normal file
22
Apis/ADS.Host/Auth/StubAdsClientCredentialValidator.cs
Normal 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."
|
||||
});
|
||||
}
|
||||
}
|
||||
20
Apis/ADS.Host/Auth/StubAdsUserCredentialValidator.cs
Normal file
20
Apis/ADS.Host/Auth/StubAdsUserCredentialValidator.cs
Normal 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."
|
||||
});
|
||||
}
|
||||
}
|
||||
29
Apis/ADS.Host/Controllers/AuthController.cs
Normal file
29
Apis/ADS.Host/Controllers/AuthController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
36
Apis/ADS.Host/Controllers/WhoAmIController.cs
Normal file
36
Apis/ADS.Host/Controllers/WhoAmIController.cs
Normal 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
125
Apis/ADS.Host/Program.cs
Normal 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();
|
||||
23
Apis/ADS.Host/Properties/launchSettings.json
Normal file
23
Apis/ADS.Host/Properties/launchSettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
Apis/ADS.Host/appsettings.json
Normal file
62
Apis/ADS.Host/appsettings.json
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user