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.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Helpers\ADS.Helpers.Middleware\ADS.Helpers.Middleware.csproj" />
<ProjectReference Include="..\..\Helpers\ADS.Helpers.Data\ADS.Helpers.Data.csproj" />
<ProjectReference Include="..\..\Helpers\ADS.Helpers.Auth\ADS.Helpers.Auth.csproj" />
</ItemGroup>
</Project>

View File

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

View File

@@ -0,0 +1,13 @@
namespace ADS.Apis.Gps.Application.Abstractions.Security;
public interface IAdsClientCredentialValidator
{
Task<AdsClientCredentialResult> ValidateAsync(string clientId, string clientSecret, CancellationToken ct = default);
}
public sealed record AdsClientCredentialResult(
bool IsValid,
int? ClientId,
string? ClientKey,
string? ClientName
);

View File

@@ -0,0 +1,13 @@
namespace ADS.Apis.Gps.Application.Abstractions.Security;
public interface IAdsUserCredentialValidator
{
Task<AdsUserCredentialResult> ValidateAsync(string username, string password, CancellationToken ct = default);
}
public sealed record AdsUserCredentialResult(
bool IsValid,
int? UserId,
string? Username,
string? DisplayName
);

View File

@@ -0,0 +1,6 @@
namespace ADS.Apis.Gps.Application.Common.Crypto;
public interface ILegacyPasswordEncryptor
{
string Encrypt(string plainText);
}

View File

@@ -0,0 +1,38 @@
using System.Security.Cryptography;
using System.Text;
namespace ADS.Apis.Gps.Application.Common.Crypto;
public static class Md5Hex32
{
public static bool IsMd5Hex32(string value)
{
if (string.IsNullOrWhiteSpace(value) || value.Length != 32) return false;
for (var i = 0; i < value.Length; i++)
{
var c = value[i];
var isHex =
(c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F');
if (!isHex) return false;
}
return true;
}
public static string ToMd5Hex32(string plainText)
{
using var md5 = MD5.Create();
var bytes = Encoding.UTF8.GetBytes(plainText ?? string.Empty);
var hash = md5.ComputeHash(bytes);
var sb = new StringBuilder(32);
foreach (var b in hash)
sb.Append(b.ToString("x2"));
return sb.ToString();
}
}

View File

@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using ADS.Apis.Gps.Application.Dtos.Auth;
namespace ADS.Apis.Gps.Application.Contracts.Auth;
public interface ISicrepAuthService
{
Task<SicrepAuthResponse> CreateTokenAsync(SicrepAuthRequest request, string? ip, string? mac, string? grupoEmpresa, CancellationToken ct = default);
}

View File

@@ -0,0 +1,20 @@
namespace ADS.Apis.Gps.Contracts.Common;
public sealed class ApiResponse<T>
{
public int CountRecords { get; init; }
public int StatusCode { get; init; }
public string StatusCodeText { get; init; } = string.Empty;
public string? ConsoleMessage { get; init; } // mensaje consola
public UiMessage? ScreenMessage { get; init; } // mensaje pantalla
public T? Value { get; init; }
}
public sealed class UiMessage
{
public int MessageType { get; init; } // tipoMensaje
public string Message { get; init; } = ""; // mensaje
public int MessageCode { get; init; } // codigoMsg
}

View File

@@ -0,0 +1,11 @@
namespace ADS.Apis.Gps.Contracts.Common;
public sealed class PagedQuery
{
public string? Search { get; init; }
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 50;
// Si Pagination=false, tu servicio puede ignorar Page/PageSize y devolver todo (o un límite fijo).
public bool Pagination { get; init; } = true;
}

View File

@@ -0,0 +1,12 @@
namespace ADS.Apis.Gps.Contracts.Common;
public sealed class RequestOf<T>
{
public T? Value { get; init; }
// Si también quieres paginación en algunos body requests:
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,10 @@
namespace ADS.Apis.Gps.Application.Dtos.Auth;
public sealed class SicrepAuthRequest
{
public string Usuario { get; set; } = string.Empty;
public string Contrasena { get; set; } = string.Empty;
// Opcional: si quieres forzar si viene en MD5 o plaintext
// public bool IsMd5Hex32 { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace ADS.Apis.Gps.Application.Dtos.Auth;
public sealed class SicrepAuthResponse
{
public string AccessToken { get; init; } = string.Empty;
public string TokenType { get; init; } = "Bearer";
}

View File

@@ -0,0 +1,13 @@
namespace ADS.Apis.Gps.Application.Units;
public interface IUnitLocationQueryService
{
Task<UnitLastLocationResponse?> GetLastLocationByNoEconomicoAsync(
int cveUsuario,
string noEconomico,
CancellationToken ct = default);
Task<IReadOnlyList<UnitClientLocationResponse>> GetLocationsByClientAsync(
int cveUsuario,
int cveCliente,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,12 @@
namespace ADS.Apis.Gps.Application.Units;
public sealed class UnitClientLocationResponse
{
public string? NoEconomico { get; set; }
public decimal? Velocidad { get; set; }
public bool? Encendido { get; set; }
public DateTime? FechaTrama { get; set; }
public string? Punto { get; set; }
public double? Longitud { get; set; }
public double? Latitud { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace ADS.Apis.Gps.Application.Units;
public sealed class UnitLastLocationResponse
{
public string? NoEconomico { get; set; }
public decimal? Velocidad { get; set; }
public bool? Encendido { get; set; }
public DateTime? FechaTrama { get; set; }
public string? Punto { get; set; }
public double? Longitud { get; set; }
public double? Latitud { get; set; }
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using ADS.Apis.Gps.Application.Common.Crypto;
using ADS.Apis.Gps.Application.Contracts.Auth;
using ADS.Apis.Gps.Application.Dtos.Auth;
using ADS.Apis.Gps.Domain.Ports.Sicrep;
using ADS.Helpers.MultiTenant.Abstractions;
// AJUSTA el namespace real de IAdsTokenService
using ADS.Helpers.Auth.Abstractions;
using ADS.Helpers.MultiTenant.Context;
namespace ADS.Apis.Gps.Application.UseCases.Auth;
public sealed class SicrepAuthService : ISicrepAuthService
{
private readonly ISicrepUserValidationRepository _repo;
private readonly ITenantContextAccessor _tenantAccessor;
private readonly IAdsTokenService _tokenService;
private readonly ILegacyPasswordEncryptor _passwordEncryptor;
public SicrepAuthService(
ISicrepUserValidationRepository repo,
ITenantContextAccessor tenantAccessor,
IAdsTokenService tokenService,
ILegacyPasswordEncryptor passwordEncryptor)
{
_repo = repo;
_tenantAccessor = tenantAccessor;
_tokenService = tokenService;
_passwordEncryptor = passwordEncryptor;
}
public async Task<SicrepAuthResponse> CreateTokenAsync(SicrepAuthRequest request,
string? ip,
string? mac,
string? grupoEmpresa, CancellationToken ct = default)
{
if (request is null) throw new ArgumentNullException(nameof(request));
var tenant = _tenantAccessor.Current;
if (tenant is null)
throw new UnauthorizedAccessException("TenantContext not available. Ensure UseInternalTenantDb() is configured.");
var cveEmpresa = tenant.EmpresaId;
var cveCliente = tenant.ClienteId;
var tenantId = tenant.TenantId;
if (string.IsNullOrWhiteSpace(request.Usuario))
throw new ArgumentException("Usuario is required.", nameof(request.Usuario));
if (string.IsNullOrWhiteSpace(request.Contrasena))
throw new ArgumentException("Contrasena is required.", nameof(request.Contrasena));
// NOTA MD5:
// No puedo asegurar si el SP espera MD5 hex 32 o plaintext.
// Estrategia segura:
// - Si ya viene con 32 hex -> se manda tal cual.
// - Si no -> se manda MD5.
// Si tu sistema espera plaintext, cambia esta línea.
var contrasena32 = _passwordEncryptor.Encrypt(request.Contrasena);/*Md5Hex32.IsMd5Hex32(request.Contrasena)
? request.Contrasena
: Md5Hex32.ToMd5Hex32(request.Contrasena);*/
var result = await _repo.ValidateUserAsync(request.Usuario, contrasena32, cveEmpresa,ip,mac,grupoEmpresa, ct);
if (result is null || result.CveUsuario <= 0 || result.Acceso != 1)
throw new UnauthorizedAccessException("Invalid credentials or access denied.");
var claims = new List<Claim>
{
new("CveUsuario", result.CveUsuario.ToString()),
new("CveEmpresa", cveEmpresa.ToString()),
new("CveCliente", cveCliente.ToString())
};
if (result.CveRol is not null)
claims.Add(new("CveRol", result.CveRol.Value.ToString()));
var token = await _tokenService.IssueFromClaimsAsync(claims, scope: null, ct);
return new SicrepAuthResponse
{
AccessToken = token.AccessToken
};
}
}

View File

@@ -0,0 +1,117 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using ADS.Apis.Gps.Application.Contracts.Auth;
using ADS.Apis.Gps.Application.Dtos.Auth;
using Microsoft.AspNetCore.Mvc;
namespace ADS.Apis.Gps.Controllers.Sicrep;
[ApiController]
[Route("gps/sicrep/auth")]
public sealed class SicrepAuthController : ControllerBase
{
private readonly ISicrepAuthService _service;
public SicrepAuthController(ISicrepAuthService service)
{
_service = service;
}
[HttpPost("token")]
public async Task<ActionResult<SicrepAuthResponse>> CreateToken(
[FromBody] SicrepAuthRequest request,
CancellationToken ct)
{
var ip = GetClientIp(HttpContext);
var mac = HttpContext.Request.Headers["X-Device-Mac"].ToString(); // si tu app lo manda
var grupoEmpresa = HttpContext.Request.Headers["X-GrupoEmpresa"].ToString();
var response = await _service.CreateTokenAsync(request,ip,mac,grupoEmpresa, ct);
return Ok(response);
}
static string? GetClientIp(HttpContext http)
{
// 1) Si hay proxy, X-Forwarded-For trae una lista: "client, proxy1, proxy2"
if (http.Request.Headers.TryGetValue("X-Forwarded-For", out var xff))
{
var first = xff.ToString().Split(',')[0].Trim();
if (!string.IsNullOrWhiteSpace(first)) return first;
}
// 2) fallback
return http.Connection.RemoteIpAddress?.ToString();
}
// // ========= UNITS =========
// [HttpGet("units")]
// [ProducesResponseType(typeof(PagedResponse<UnitListItem>), StatusCodes.Status200OK)]
// public Task<PagedResponse<UnitListItem>> GetUnits(
// [FromQuery] string? search,
// [FromQuery] int page = 1,
// [FromQuery] int pageSize = 50,
// CancellationToken ct = default)
// => _svc.GetUnitsAsync(search, page, pageSize, ct);
// [HttpGet("units/{unitId:int}")]
// [ProducesResponseType(typeof(UnitDetail), StatusCodes.Status200OK)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// public async Task<ActionResult<UnitDetail>> GetUnit([FromRoute] int unitId, CancellationToken ct = default)
// {
// var dto = await _svc.GetUnitAsync(unitId, ct);
// return dto is null ? NotFound() : Ok(dto);
// }
// [HttpGet("units/{unitId:int}/location")]
// [ProducesResponseType(typeof(UnitLocation), StatusCodes.Status200OK)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// public async Task<ActionResult<UnitLocation>> GetLastLocation([FromRoute] int unitId, CancellationToken ct = default)
// {
// var dto = await _svc.GetLastLocationAsync(unitId, ct);
// return dto is null ? NotFound() : Ok(dto);
// }
// [HttpGet("units/{unitId:int}/positions")]
// [ProducesResponseType(typeof(PositionsResponse), StatusCodes.Status200OK)]
// public Task<PositionsResponse> GetPositions(
// [FromRoute] int unitId,
// [FromQuery] DateTime from,
// [FromQuery] DateTime to,
// [FromQuery] int limit = 5000,
// CancellationToken ct = default)
// => _svc.GetPositionsAsync(unitId, from, to, limit, ct);
// [HttpGet("units/{unitId:int}/events")]
// [ProducesResponseType(typeof(EventsResponse), StatusCodes.Status200OK)]
// public Task<EventsResponse> GetEvents(
// [FromRoute] int unitId,
// [FromQuery] DateTime from,
// [FromQuery] DateTime to,
// CancellationToken ct = default)
// => _svc.GetEventsAsync(unitId, from, to, ct);
// [HttpGet("units/{unitId:int}/status")]
// [ProducesResponseType(typeof(UnitStatus), StatusCodes.Status200OK)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// public async Task<ActionResult<UnitStatus>> GetStatus([FromRoute] int unitId, CancellationToken ct = default)
// {
// var dto = await _svc.GetStatusAsync(unitId, ct);
// return dto is null ? NotFound() : Ok(dto);
// }
// [HttpPost("units/{unitId:int}/status")]
// [ProducesResponseType(typeof(UnitStatus), StatusCodes.Status200OK)]
// public Task<UnitStatus> SetStatus(
// [FromRoute] int unitId,
// [FromBody] UnitStatusRequest body,
// CancellationToken ct = default)
// => _svc.SetStatusAsync(unitId, body, ct);
// [HttpGet("units/{unitId:int}/health")]
// [ProducesResponseType(typeof(UnitHealth), StatusCodes.Status200OK)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// public async Task<ActionResult<UnitHealth>> GetHealth([FromRoute] int unitId, CancellationToken ct = default)
// {
// var dto = await _svc.GetHealthAsync(unitId, ct);
// return dto is null ? NotFound() : Ok(dto);
// }
}

View File

@@ -0,0 +1,57 @@
using ADS.Apis.Gps.Application.Units;
using ADS.Helpers.Middleware.InternalUser;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ADS.Apis.Gps.Controllers;
[ApiController]
[Route("units")]
public sealed class UnitsController : ControllerBase
{
private readonly IUnitLocationQueryService _unitLocationQueryService;
private readonly IInternalUserContextAccessor _internalUserContextAccessor;
public UnitsController(
IUnitLocationQueryService unitLocationQueryService,
IInternalUserContextAccessor internalUserContextAccessor)
{
_unitLocationQueryService = unitLocationQueryService;
_internalUserContextAccessor = internalUserContextAccessor;
}
[HttpGet("location/{noEconomico}")]
public async Task<IActionResult> GetLastLocationByNoEconomico(
[FromRoute] string noEconomico,
CancellationToken ct)
{
var cveUsuario = _internalUserContextAccessor.Current?.CveUser;
if (cveUsuario is null || cveUsuario <= 0)
return Unauthorized("No se encontró el usuario interno.");
var result = await _unitLocationQueryService
.GetLastLocationByNoEconomicoAsync(cveUsuario.Value, noEconomico, ct);
if (result is null)
return NotFound();
return Ok(result);
}
[HttpGet("client/{cveCliente}")]
public async Task<IActionResult> GetLocationsByClient(
[FromRoute] int cveCliente,
CancellationToken ct)
{
var cveUsuario = _internalUserContextAccessor.Current?.CveUser;
if (cveUsuario is null || cveUsuario <= 0)
return Unauthorized("No se encontró el usuario interno.");
var result = await _unitLocationQueryService
.GetLocationsByClientAsync(cveUsuario.Value, cveCliente, ct);
return Ok(result);
}
}

View File

@@ -0,0 +1,11 @@
namespace ADS.Apis.Gps.Domain.Models.Sicrep;
public sealed class SicrepUserValidationResult
{
public int CveUsuario { get; init; }
public int Acceso { get; init; }
public int? CveRol { get; init; }
// Si tu SP regresa más campos, agrégalos aquí (p.ej. nombre, email, etc.)
}

View File

@@ -0,0 +1,17 @@
using System.Threading;
using System.Threading.Tasks;
using ADS.Apis.Gps.Domain.Models.Sicrep;
namespace ADS.Apis.Gps.Domain.Ports.Sicrep;
public interface ISicrepUserValidationRepository
{
Task<SicrepUserValidationResult?> ValidateUserAsync(
string usuario,
string contrasena32,
int cveEmpresa,
string? ip,
string? mac,
string? grupoEmpresa,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,8 @@
namespace ADS.Apis.Gps.Infrastructure.Crypto;
public sealed class LegacyCryptoOptions
{
public string Mode { get; set; } = "AES"; // "DES" o "AES"
public string Key { get; set; } = "";
public string IV { get; set; } = ""; // Base64
}

View File

@@ -0,0 +1,69 @@
using System.Security.Cryptography;
using System.Text;
using ADS.Apis.Gps.Application.Common.Crypto;
using Microsoft.Extensions.Options;
namespace ADS.Apis.Gps.Infrastructure.Crypto;
public sealed class LegacyPasswordEncryptor : ILegacyPasswordEncryptor
{
private readonly LegacyCryptoOptions _opt;
public LegacyPasswordEncryptor(IOptions<LegacyCryptoOptions> opt)
=> _opt = opt.Value;
public string Encrypt(string plainText)
{
if (string.IsNullOrEmpty(plainText)) return string.Empty;
var mode = (_opt.Mode ?? "AES").Trim().ToUpperInvariant();
var iv = Convert.FromBase64String(_opt.IV ?? "");
return mode switch
{
"DES" => EncryptDesBase64(plainText, _opt.Key, iv),
"AES" => EncryptAesBase64(plainText, _opt.Key, iv),
_ => throw new InvalidOperationException($"Unsupported LegacyCrypto.Mode: {_opt.Mode}")
};
}
private static string EncryptDesBase64(string plainText, string keySource, byte[] iv8)
{
if (iv8.Length != 8) throw new ArgumentException("DES IV must be 8 bytes.", nameof(iv8));
var key8 = Encoding.UTF8.GetBytes((keySource ?? string.Empty).PadRight(8).Substring(0, 8));
using var des = new DESCryptoServiceProvider
{
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7,
Key = key8,
IV = iv8
};
var input = Encoding.UTF8.GetBytes(plainText);
using var encryptor = des.CreateEncryptor(des.Key, des.IV);
var cipher = encryptor.TransformFinalBlock(input, 0, input.Length);
return Convert.ToBase64String(cipher);
}
private static string EncryptAesBase64(string plainText, string keySource, byte[] iv16)
{
if (iv16.Length != 16) throw new ArgumentException("AES IV must be 16 bytes.", nameof(iv16));
var key32 = Encoding.UTF8.GetBytes((keySource ?? string.Empty).PadRight(32).Substring(0, 32));
using var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = key32;
aes.IV = iv16;
var input = Encoding.UTF8.GetBytes(plainText);
using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
var cipher = encryptor.TransformFinalBlock(input, 0, input.Length);
return Convert.ToBase64String(cipher);
}
}

View File

@@ -0,0 +1,57 @@
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using ADS.Apis.Gps.Domain.Models.Sicrep;
using ADS.Apis.Gps.Domain.Ports.Sicrep;
using ADS.Helpers.Data.Abstractions;
using ADS.Helpers.MultiTenant.Abstractions;
using ADS.Helpers.MultiTenant.Context;
using Dapper;
namespace ADS.Apis.Gps.Infrastructure.Data.Dapper;
public sealed class DapperSicrepUserValidationRepository : ISicrepUserValidationRepository
{
private readonly ITenantDbConnectionFactory _connFactory;
private readonly ITenantContextAccessor _tenantAccessor;
public DapperSicrepUserValidationRepository(
ITenantDbConnectionFactory connFactory,
ITenantContextAccessor tenantAccessor)
{
_connFactory = connFactory;
_tenantAccessor = tenantAccessor;
}
public async Task<SicrepUserValidationResult?> ValidateUserAsync(
string usuario,
string contrasena32,
int cveEmpresa,
string? ip,
string? mac,
string? grupoEmpresa,
CancellationToken ct = default)
{
var tenant = _tenantAccessor.Current
?? throw new UnauthorizedAccessException("TenantContext not available.");
// USAMOS TU MÉTODO REAL
using var conn = _connFactory.Create();//tenant.Database.ConnectionString);
var p = new DynamicParameters();
p.Add("@Usuario", usuario, DbType.String, size: 32);
p.Add("@Contraseña", contrasena32, DbType.String, size: 32);
p.Add("@CveEmpresa", cveEmpresa, DbType.Int32);
p.Add("@IP", ip ?? string.Empty, DbType.String, size: 20);
p.Add("@MAC", mac ?? string.Empty, DbType.String, size: 50);
p.Add("@GrupoEmpresa", grupoEmpresa ?? string.Empty, DbType.String, size: 100);
const string sp = "[dbo].[PA_Sis_UsuarioValidacionGet]";
var result = await conn.QueryFirstOrDefaultAsync<SicrepUserValidationResult>(
sp,
p,
commandType: CommandType.StoredProcedure);
return result;
}
}

View File

@@ -0,0 +1,34 @@
using ADS.Apis.Gps.Application.Abstractions.Security;
using ADS.Apis.Gps.Application.Common.Crypto;
using ADS.Apis.Gps.Application.Contracts.Auth;
using ADS.Apis.Gps.Application.UseCases.Auth;
using ADS.Apis.Gps.Domain.Ports.Sicrep;
using ADS.Apis.Gps.Infrastructure.Crypto;
using ADS.Apis.Gps.Infrastructure.Data.Dapper;
using ADS.Apis.Gps.Infrastructure.Security;
using Microsoft.Extensions.DependencyInjection;
namespace ADS.Apis.Gps.Infrastructure.DependencyInjection;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddGpsSicrepAuth(this IServiceCollection services)
{
services.AddScoped<IAdsUserCredentialValidator, AdsUserCredentialValidator>();
services.AddScoped<IAdsClientCredentialValidator, AdsClientCredentialValidator>();
// Domain Ports -> Infrastructure
services.AddScoped<ISicrepUserValidationRepository, DapperSicrepUserValidationRepository>();
// Application
services.AddScoped<ISicrepAuthService, SicrepAuthService>();
return services;
}
public static IServiceCollection AddGpsInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<LegacyCryptoOptions>(configuration.GetSection("LegacyCrypto"));
services.AddSingleton<ILegacyPasswordEncryptor, LegacyPasswordEncryptor>();
return services;
}
}

View File

@@ -0,0 +1,45 @@
using System.Data;
using Dapper;
using ADS.Apis.Gps.Application.Abstractions.Security;
using ADS.Helpers.Data.Abstractions;
namespace ADS.Apis.Gps.Infrastructure.Security;
public sealed class AdsClientCredentialValidator : IAdsClientCredentialValidator
{
private readonly ITenantDbConnectionFactory _db;
public AdsClientCredentialValidator(ITenantDbConnectionFactory db)
{
_db = db;
}
public async Task<AdsClientCredentialResult> ValidateAsync(string clientId, string clientSecret, CancellationToken ct = default)
{
// using var conn = await _db.OpenAsync(ct);
// var p = new DynamicParameters();
// p.Add("@ClientId", clientId, DbType.String);
// p.Add("@ClientSecret", clientSecret, DbType.String);
// var row = await conn.QueryFirstOrDefaultAsync<ClientRow>(
// sql: "PA_Sicrep_ValidateClient",
// param: p,
// commandType: CommandType.StoredProcedure
// );
// if (row is null || row.IsValid == 0)
// return new AdsClientCredentialResult(false, null, null, null);
// return new AdsClientCredentialResult(true, row.ClientId, row.ClientKey, row.ClientName);
return null;
}
private sealed class ClientRow
{
public int IsValid { get; init; }
public int ClientId { get; init; }
public string ClientKey { get; init; } = string.Empty;
public string ClientName { get; init; } = string.Empty;
}
}

View File

@@ -0,0 +1,47 @@
using System.Data;
using Dapper;
using ADS.Apis.Gps.Application.Abstractions.Security;
using ADS.Helpers.Data.Abstractions;
namespace ADS.Apis.Gps.Infrastructure.Security;
public sealed class AdsUserCredentialValidator : IAdsUserCredentialValidator
{
private readonly ITenantDbConnectionFactory _db;
public AdsUserCredentialValidator(ITenantDbConnectionFactory db)
{
_db = db;
}
public async Task<AdsUserCredentialResult> ValidateAsync(string username, string password, CancellationToken ct = default)
{
// using var conn = await _db.OpenAsync(ct);
// // EJEMPLO: si validas con SP
// // Cambia el nombre del SP y los nombres de parámetros a tus reales.
// var p = new DynamicParameters();
// p.Add("@Usuario", username, DbType.String);
// p.Add("@Password", password, DbType.String);
// var row = await conn.QueryFirstOrDefaultAsync<UserRow>(
// sql: "PA_Sicrep_ValidateUser",
// param: p,
// commandType: CommandType.StoredProcedure
// );
// if (row is null || row.IsValid == 0)
// return new AdsUserCredentialResult(false, null, null, null);
// return new AdsUserCredentialResult(true, row.UserId, row.Username, row.DisplayName);
return null;
}
private sealed class UserRow
{
public int IsValid { get; init; }
public int UserId { get; init; }
public string Username { get; init; } = string.Empty;
public string DisplayName { get; init; } = string.Empty;
}
}

View File

@@ -0,0 +1,60 @@
using System.Data;
using ADS.Apis.Gps.Application.Units;
using ADS.Helpers.Data;
using ADS.Helpers.Data.Abstractions;
using Dapper;
namespace ADS.Apis.Gps.Infrastructure.Units;
public sealed class UnitLocationQueryService : IUnitLocationQueryService
{
private readonly ITenantDbConnectionFactory _connectionFactory;
public UnitLocationQueryService(ITenantDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<UnitLastLocationResponse?> GetLastLocationByNoEconomicoAsync(
int cveUsuario,
string noEconomico,
CancellationToken ct = default)
{
using var connection = _connectionFactory.Create();
var command = new CommandDefinition(
commandText: "dbo.PA_GPS_DispositovoUbicacionByNoEconomicoGet",
parameters: new
{
CveUsuario = cveUsuario,
NoEconomico = noEconomico
},
commandType: CommandType.StoredProcedure,
cancellationToken: ct
);
return await connection.QueryFirstOrDefaultAsync<UnitLastLocationResponse>(command);
}
public async Task<IReadOnlyList<UnitClientLocationResponse>> GetLocationsByClientAsync(
int cveUsuario,
int cveCliente,
CancellationToken ct = default)
{
using var connection = _connectionFactory.Create();
var command = new CommandDefinition(
commandText: "dbo.PA_GPS_DispositovoUbicacionPorClienteGet",
parameters: new
{
CveUsuario = cveUsuario,
CveCliente = cveCliente
},
commandType: CommandType.StoredProcedure,
cancellationToken: ct
);
var result = await connection.QueryAsync<UnitClientLocationResponse>(command);
return result.AsList();
}
}

View File

@@ -0,0 +1,49 @@
using ADS.Helpers.Data.Extensions;
using ADS.Helpers.MultiTenant.InternalRouting;
using ADS.Helpers.Auth.Extensions;
using ADS.Helpers.Middleware.ADSExtensions;
using ADS.Helpers.Middleware.Extensions;
using ADS.Helpers.Middleware.InternalUser;
using ADS.Apis.Gps.Application.Units;
using ADS.Apis.Gps.Infrastructure.Units;
// using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAdsApiResponse(builder.Configuration);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// builder.Services.AddAdsAuthentication(builder.Configuration);
// builder.Services.AddAdsAuthorization();
// ✅ 1) Middleware interno: valida HMAC + resuelve tenant (debe llenar ITenantContextAccessor.Current)
builder.Services.AddInternalTenantDb(builder.Configuration);
builder.Services.AddScoped<IInternalUserContextAccessor, InternalUserContextAccessor>();
builder.Services.AddScoped<IUnitLocationQueryService, UnitLocationQueryService>();
// ✅ 2) Factory transversal (usa tenant.Database.ConnectionString)
builder.Services.AddAdsTenantDb();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
}
app.UseSwagger();
app.UseSwaggerUI();
app.UseRouting();
// primero exception middleware (para atrapar todo)
app.UseAdsApiResponse();
// ✅ HMAC + set TenantContext
app.UseInternalTenantDb();
app.UseAdsInternalUser();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
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:5023",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7134;http://localhost:5023",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,27 @@
{
"InternalRouting": {
"SharedSecret": "MISMO_SECRETOJCAUDIADS_DEL_HOST",
"MaxSkewSeconds": 120
},
"Jwt": {
"Issuer": "ADS",
"Audience": "ADS",
"SigningKey": "SUPER_JCau127*953SECRET_KEY_CHANGE_THIS",
"AccessTokenMinutes": 60
},
"LegacyCrypto": {
"Mode": "DES",
"Key": "$Ap7+5&z",
"IV": "1234567890ABCDEF"
},
"LoggingOptions": {
"LogFolder": "C:\\Logs\\ADS\\Gps"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,30 @@
Quiero rehacer desde cero la API ADS.Apis.Gps con orden limpio (Domain / Application / Infrastructure / Controllers) y multi-tenant por headers internos desde el Host.
Contexto:
- El Host (gateway) resuelve tenant y manda headers internos firmados (InternalRouting).
- En el micro GPS ya uso UseInternalTenantDb() para validar firma y poblar TenantContext.
- TenantContext tiene: EmpresaId, ClienteId, TenantId (string), Culture, Theme, Database { Provider, ConnectionString }, CustomSettings.
- GPS NO debe leer appsettings para tenants ni duplicar config del Host. NO usar ITenantStore en GPS.
- La conexión a BD siempre sale de TenantContext.Database.ConnectionString y se abre vía ITenantDbConnectionFactory (ADS.Helpers.Data).
Objetivo inmediato:
1) Definir la estructura de carpetas y proyectos dentro de ADS.Apis.Gps:
- Domain: modelos/puertos (interfaces) sin SQL ni Dapper.
- Application: casos de uso/servicios y orquestación.
- Infrastructure: Dapper/SQL repos/stores.
- Controllers: endpoints.
2) Implementar autenticación Sicrep en GPS:
- Endpoint POST /gps/sicrep/auth/token
- Antes de generar JWT, ejecutar el SP [dbo].[PA_Sis_UsuarioValidacionGet] con @Usuario, @Contraseña (varchar(32)), @CveEmpresa (del tenant), y validar que CveUsuario > 0 y Acceso = 1.
- Luego emitir JWT usando ADS.Helpers.Auth (IAdsTokenService) con TenantId string y claims (CveUsuario, CveEmpresa, CveCliente, CveRol, etc.)
3) Entregarme:
- Árbol final de carpetas.
- Código completo de: puertos (interfaces), modelos domain, repos Infrastructure (Dapper + SP), servicio Application (caso de uso), controller, y registros DI en Program.cs.
- NOTA: No uses GetConnectionString("...") ni appsettings para la DB del micro.
Datos extra:
- Confirmar si la contraseña en BD ya viene en MD5 hex 32 o si debo hashear antes de llamar al SP. (Si no sabes, deja helper para MD5 y comenta dónde se decide).
IMPORTANTE: No inventes métodos que no existan; si necesitas agregar algo a ADS.Helpers.Auth o ADS.Helpers.MultiTenant/Helpers.Data, dilo explícitamente y dame el código exacto del archivo a modificar.
pasame un script para la generacion de archivos y carpetas
tampoco te inventes rutas nuevas las apis las estoy guardando en la carpeta de apis
la ruta del proyecto debe ser exatamente la misma para que funciione con ADS.Apis.slnx