feat: services, jwt auth, and api controllers

- LeadService with 15 SP calls (CRUD, actividades, pagos, montos)
- UsuarioService for login and profile queries
- JwtService for token generation with claims
- AuthController (login, perfil) with HttpOnly cookie
- LeadController (CRUD, actividades, estado, pago, contacto)
- Registered all services in DI (Program.cs)
This commit is contained in:
2026-07-07 17:56:25 -04:00
parent 3d5419403d
commit aed251d82f
7 changed files with 445 additions and 15 deletions
+18 -15
View File
@@ -14,9 +14,9 @@
| Backend .NET 10 — Skeleton (sln + 4 proyectos + NuGet) | ✅ CREADO |
| Entidades / DTOs / Interfaces (Ventas.Core) | ✅ CREADO |
| Infrastructure (EF Core + Dapper + Repos) | ✅ PARCIAL (DbContext + LeadRepository listos) |
| Services (lógica de negocio) | PENDIENTE |
| API Controllers | PENDIENTE |
| Auth JWT | ❌ PENDIENTE |
| Services (lógica de negocio) | PARCIAL (LeadService, UsuarioService, JwtService) |
| API Controllers | PARCIAL (AuthController, LeadController) |
| Auth JWT | ✅ CREADO (JwtService + middleware) |
| Reportes QuestPDF (17 reportes) | ❌ PENDIENTE |
| ServicesExternos.API | ❌ PENDIENTE |
| Frontend Next.js | ❌ PENDIENTE |
@@ -42,17 +42,19 @@
- [ ] Configurations/ (EF mapping)
- [ ] Resto repos (Alumno, Contrato, Cotizacion, etc.)
- [ ] SpComplexQueries.cs (Dapper multi-resultset)
- [ ] **1.4 Ventas.Services — Lógica de Negocio** (~2 sem)
- [ ] LeadService.cs + resto servicios (~15)
- [ ] **1.5 Ventas.API — Controladores REST** (~1 sem)
- [ ] AuthController.cs
- [ ] LeadController.cs + resto controllers (~20)
- [ ] Program.cs completo (DI, JWT, CORS)
- [ ] appsettings.json con connection strings
- [ ] **1.6 Auth JWT** (~3 días)
- [ ] JwtMiddleware.cs
- [ ] Login endpoint funcional
- [ ] Claims → cookies HttpOnly
- [x] **1.4 Ventas.Services — Lógica de Negocio** (~2 sem)
- [x] LeadService.cs, UsuarioService.cs, JwtService.cs
- [ ] Resto servicios (Contrato, Cotizacion, Alumno, etc.)
- [x] **1.5 Ventas.API — Controladores REST** (~1 sem)
- [x] AuthController.cs (login + perfil)
- [x] LeadController.cs (CRUD + actividades + pagos)
- [x] Program.cs completo (DI, JWT, CORS)
- [x] appsettings.json con connection strings
- [ ] Resto controllers (Contrato, Cotizacion, Alumno, etc.)
- [x] **1.6 Auth JWT** (~3 días)
- [x] JwtService.cs (generación de tokens)
- [x] Login endpoint funcional
- [x] Claims → cookies HttpOnly
- [ ] **1.7 Reportes QuestPDF** (~2-3 sem)
- [ ] ReportService.cs (base)
- [ ] 17 reportes (Contrato, Cotización, Arqueo, etc.)
@@ -103,7 +105,8 @@
| Fecha | Fase | Qué se hizo | Siguiente paso |
|-------|------|-------------|----------------|
| 2026-07-07 | F1 | Entities (25), Enums (4), DTOs, Interfaces (6), DbContext, LeadRepository, LeadQueryRepository, DI setup, appsettings | Resto repos (Contrato, Cotizacion, Alumno) + Controllers |
| 2026-07-07 | F1 | Entities (25), Enums, DTOs, Interfaces, DbContext, LeadRepository + LeadQueryRepository, DI, JWT, appsettings | Resto repos + Controllers |
| 2026-07-07 | F1 | LeadService, UsuarioService, JwtService, AuthController, LeadController | Resto Services + Controllers |
| | | | |
| | | | |
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc;
using Ventas.Core.DTOs;
using Ventas.Services;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly UsuarioService _usuarioService;
private readonly JwtService _jwtService;
public AuthController(UsuarioService usuarioService, JwtService jwtService)
{
_usuarioService = usuarioService;
_jwtService = jwtService;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var usuario = await _usuarioService.LoginAsync(request);
if (usuario == null)
return Unauthorized(new { mensaje = "Credenciales inválidas" });
var token = _jwtService.GenerateToken(request.Rut, usuario.Nombre, usuario.Sede ?? "");
Response.Cookies.Append("SAM_TOKEN", token, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTime.UtcNow.AddMinutes(30)
});
return Ok(new
{
nombre = usuario.Nombre,
sede = usuario.Sede,
token
});
}
[HttpGet("perfil")]
public async Task<IActionResult> Perfil([FromQuery] string usuarioId)
{
var perfil = await _usuarioService.PerfilAsync(usuarioId);
if (perfil == null)
return NotFound();
return Ok(perfil);
}
}
@@ -0,0 +1,110 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ventas.Core.DTOs;
using Ventas.Services;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class LeadController : ControllerBase
{
private readonly LeadService _leadService;
public LeadController(LeadService leadService)
{
_leadService = leadService;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var result = await _leadService.BuscarIDAsync(id);
return Ok(result);
}
[HttpGet("buscar")]
public async Task<IActionResult> Buscar([FromQuery] int ejecutivo, [FromQuery] int estado, [FromQuery] int dias)
{
var result = await _leadService.BuscarAsync(ejecutivo, estado, dias);
return Ok(result);
}
[HttpGet("nuevos")]
public async Task<IActionResult> Nuevos([FromQuery] int ejecutivo)
{
var result = await _leadService.BuscarNuevosAsync(ejecutivo);
return Ok(result);
}
[HttpGet("{id}/actividades")]
public async Task<IActionResult> Actividades(int id, [FromQuery] string tipo)
{
var result = await _leadService.ActividadesAsync(id, tipo);
return Ok(result);
}
[HttpGet("montos")]
public async Task<IActionResult> Montos([FromQuery] int ejecutivo)
{
var result = await _leadService.MontosAsync(ejecutivo);
return Ok(result);
}
[HttpGet("motivos-perdido")]
public async Task<IActionResult> MotivosPerdido()
{
var result = await _leadService.MotivosPerdidoAsync();
return Ok(result);
}
[HttpPost]
public async Task<IActionResult> Ingresar([FromBody] LeadCreateDto dto)
{
var result = await _leadService.IngresarAsync(dto);
return Ok(new { mensaje = result });
}
[HttpPost("{id}/actividad")]
public async Task<IActionResult> IngresarActividad(int id, [FromBody] ActividadCreateDto dto, [FromQuery] string usuarioId)
{
var result = await _leadService.IngresarActividadAsync(id, dto.Tipo, dto.Descripcion, usuarioId);
return Ok(new { mensaje = result });
}
[HttpPut("{id}/estado")]
public async Task<IActionResult> ActualizarEstado(int id, [FromQuery] int estadoId)
{
var result = await _leadService.EstadoUpdateAsync(id, estadoId);
return Ok(new { mensaje = result });
}
[HttpPut("{id}/producto")]
public async Task<IActionResult> ActualizarProducto(int id, [FromQuery] string producto)
{
var result = await _leadService.ActualizarProductoAsync(id, producto);
return Ok(new { mensaje = result });
}
[HttpPut("{id}/contacto")]
public async Task<IActionResult> ActualizarContacto(int id, [FromBody] ContactoUpdateDto dto)
{
var result = await _leadService.ActualizarContactoAsync(id, dto.Nombre ?? "", dto.Mail ?? "", dto.Telefono ?? "", dto.Rut ?? "");
return Ok(new { mensaje = result });
}
[HttpPost("{id}/perder")]
public async Task<IActionResult> IngresarLeadPerdido(int id, [FromQuery] int motivo, [FromQuery] int estado)
{
var result = await _leadService.IngresarLeadPerdidoAsync(id, motivo, estado);
return Ok(new { mensaje = result });
}
[HttpPost("{id}/pago")]
public async Task<IActionResult> IngresarPago(int id, [FromBody] PagoLeadDto dto)
{
var result = await _leadService.IngresarPagoAsync(dto.LeadId, dto.CotizacionId, dto.FormaPago, dto.Monto, dto.CodigoAutorizacion ?? "", dto.DigitoTarjeta ?? "", dto.Cuotas);
return Ok(new { mensaje = result });
}
}
+10
View File
@@ -5,6 +5,7 @@ using Microsoft.IdentityModel.Tokens;
using Ventas.Infrastructure.Data;
using Ventas.Infrastructure.Repositories;
using Ventas.Core.Interfaces;
using Ventas.Services;
var builder = WebApplication.CreateBuilder(args);
@@ -21,7 +22,16 @@ builder.Services.AddScoped<ILeadRepository>(sp =>
builder.Services.AddScoped<ILeadQueryRepository>(sp =>
new LeadQueryRepository(connectionString));
builder.Services.AddScoped<LeadService>(sp =>
new LeadService(connectionString));
builder.Services.AddScoped<UsuarioService>(sp =>
new UsuarioService(connectionString));
var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "default-dev-secret-change-in-production";
var jwtExpiration = int.Parse(builder.Configuration["Jwt:ExpirationMinutes"] ?? "30");
builder.Services.AddScoped<JwtService>(sp =>
new JwtService(jwtSecret, jwtExpiration));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
+38
View File
@@ -0,0 +1,38 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace Ventas.Services;
public class JwtService
{
private readonly string _secret;
private readonly int _expirationMinutes;
public JwtService(string secret, int expirationMinutes)
{
_secret = secret;
_expirationMinutes = expirationMinutes;
}
public string GenerateToken(string usuarioId, string nombre, string sede)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secret));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, usuarioId),
new Claim(ClaimTypes.Name, nombre),
new Claim("sede", sede)
};
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_expirationMinutes),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
+160
View File
@@ -0,0 +1,160 @@
using Dapper;
using Npgsql;
using Ventas.Core.DTOs;
namespace Ventas.Services;
public class LeadService
{
private readonly string _connectionString;
public LeadService(string connectionString)
{
_connectionString = connectionString;
}
public async Task<IEnumerable<Dictionary<string, object>>> BuscarAsync(int ejecutivo, int estado, int dias)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.BuscarLeadV3",
new { ejecutivo, estadolead = estado, filtro = dias },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> BuscarIDAsync(int id)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.BuscarLeadID",
new { id },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> BuscarNuevosAsync(int ejecutivo)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.BuscarLeadNuevos",
new { ejecutivo },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<string> IngresarAsync(LeadCreateDto dto)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.GrabaLead",
new { nombrelead = dto.Nombre, maillead = dto.Mail, telefonolead = dto.Telefono, productolead = dto.Producto, contactolead = dto.Contacto, ejecutivoid = dto.EjecutivoId },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> IngresarV2Async(LeadCreateDto dto)
{
using var connection = new NpgsqlConnection(_connectionString);
var result = await connection.QuerySingleOrDefaultAsync<string>(
"sige_sam_v3.GrabaLead",
new { nombrelead = dto.Nombre, maillead = dto.Mail, telefonolead = dto.Telefono, productolead = dto.Producto, contactolead = dto.Contacto, ejecutivoid = dto.EjecutivoId },
commandType: System.Data.CommandType.StoredProcedure);
return result ?? "ok";
}
public async Task<string> IngresarPagoAsync(int lead, int coti, string pago, int monto, string cod, string dig, int cuota)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.GrabaPagoLead",
new { leadid = lead, cotiid = coti, formapago = pago, valor = monto, codauto = cod, digtar = dig, cantcouta = cuota },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> ActualizarProductoAsync(int leadId, string producto)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sam.ActualuzarLeadProducto",
new { leadid = leadId, productlead = producto },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<IEnumerable<Dictionary<string, object>>> MontosAsync(int ejecutivo)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.BuscarLeadMontos",
new { ejecutivo },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<IEnumerable<Dictionary<string, object>>> ActividadesAsync(int leadId, string tipo)
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.BuscarActividadesLead",
new { id = leadId, tipoactividad = tipo },
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<string> IngresarActividadAsync(int leadId, string tipo, string descripcion, string usuarioId)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.GrabaActividadesLead",
new { leadid = leadId, tipo, descripcion, usuarioid = usuarioId },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> EstadoUpdateAsync(int leadId, int estadoId)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.ActualizarEstadoLead",
new { leadid = leadId, estadolead = estadoId },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<IEnumerable<Dictionary<string, object>>> MotivosPerdidoAsync()
{
using var connection = new NpgsqlConnection(_connectionString);
var rows = await connection.QueryAsync(
"sige_sam_v3.BuscarMotivoLeadPerdido",
commandType: System.Data.CommandType.StoredProcedure);
return rows.Select(r => (Dictionary<string, object>)(IDictionary<string, object>)r!);
}
public async Task<string> IngresarLeadPerdidoAsync(int leadId, int motivo, int estado)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.Lead_Ingresar_Perdido",
new { leadid = leadId, motivo, estado },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
public async Task<string> ActualizarContactoAsync(int leadId, string nombre, string mail, string fono, string rut)
{
using var connection = new NpgsqlConnection(_connectionString);
await connection.ExecuteAsync(
"sige_sam_v3.ActualizaLeadContacto",
new { leadid = leadId, nombrecontacto = nombre, mailcontacto = mail, fonocontacto = fono, rutcontacto = rut },
commandType: System.Data.CommandType.StoredProcedure);
return "ok";
}
}
@@ -0,0 +1,55 @@
using Dapper;
using Npgsql;
using Ventas.Core.DTOs;
namespace Ventas.Services;
public class UsuarioService
{
private readonly string _connectionString;
public UsuarioService(string connectionString)
{
_connectionString = connectionString;
}
public async Task<LoginResponse?> LoginAsync(LoginRequest request)
{
using var connection = new NpgsqlConnection(_connectionString);
var data = await connection.QueryAsync(
"sam.BuscarUsuario",
new { userid = request.Rut, passid = request.Clave },
commandType: System.Data.CommandType.StoredProcedure);
var user = data.FirstOrDefault();
if (user == null)
return null;
var dict = (IDictionary<string, object>)user;
return new LoginResponse
{
Nombre = dict["Nombres"]?.ToString() ?? "",
Sede = dict["idSede"]?.ToString()
};
}
public async Task<LoginResponse?> PerfilAsync(string usuarioId)
{
using var connection = new NpgsqlConnection(_connectionString);
var data = await connection.QueryAsync(
"sige_sam_v3.BuscarPerfilUsuario",
new { usuario = usuarioId },
commandType: System.Data.CommandType.StoredProcedure);
var user = data.FirstOrDefault();
if (user == null) return null;
var dict = (IDictionary<string, object>)user;
return new LoginResponse
{
Nombre = dict["Nombres"]?.ToString() ?? "",
Sede = dict["idSede"]?.ToString(),
Perfil = dict["Perfil"]?.ToString()
};
}
}