diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 784cd7c..8e5db47 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -33,6 +33,9 @@ jobs:
- name: Build (warnings as errors)
run: dotnet build "EpycusApp.csproj" --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore -warnaserror
+ - name: Run unit tests
+ run: dotnet test "EpycusApp.Tests/EpycusApp.Tests.csproj" --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore --verbosity normal
+
# ── JOB 2: Build y publish ──────────────────────────────────────────
build:
name: Build & Publish
@@ -117,8 +120,21 @@ jobs:
target: ${{ env.VPS_APP_PATH }}
strip_components: 1
+ # Aplicar migraciones EF Core
+ - name: Apply EF Core migrations
+ uses: appleboy/ssh-action@v1.0.3
+ continue-on-error: true
+ with:
+ host: ${{ env.VPS_HOST }}
+ username: ${{ env.VPS_USER }}
+ key: ${{ secrets.VPS_SSH_KEY }}
+ port: ${{ env.VPS_PORT }}
+ script: |
+ cd ${{ env.VPS_APP_PATH }}
+ sudo dotnet ef database update --no-build 2>&1 || echo "⚠️ Migraciones no aplicadas (pueden no requerirse)"
+ echo "✅ Migraciones ejecutadas"
+
# Reinicio seguro
- - name: Restart service
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ env.VPS_HOST }}
diff --git a/Ayudantes/ConstantesGamificacion.cs b/Ayudantes/ConstantesGamificacion.cs
index 6412bec..14c1ede 100644
--- a/Ayudantes/ConstantesGamificacion.cs
+++ b/Ayudantes/ConstantesGamificacion.cs
@@ -12,8 +12,7 @@ public static class ConstantesGamificacion
public const int XP_MISION_BAJA = 30;
public const int XP_MISION_MEDIA = 50;
public const int XP_MISION_ALTA = 80;
- public const int XP_BASE_POMODORO = 15;
- public const int XP_POMODORO_POR_CICLO = 15;
+ public const int XpBasePomodoro = 15;
public const int NIVEL_MAXIMO = 20;
}
}
diff --git a/Ayudantes/DebeSerVerdaderoAttribute.cs b/Ayudantes/DebeSerVerdaderoAttribute.cs
index c88c7d3..65268d7 100644
--- a/Ayudantes/DebeSerVerdaderoAttribute.cs
+++ b/Ayudantes/DebeSerVerdaderoAttribute.cs
@@ -4,8 +4,8 @@
namespace EpycusApp.Ayudantes;
///
-/// Valida que un checkbox booleano esté marcado como true.
-/// Reemplaza [Range(typeof(bool), "true", "true")] que falla en validación
+/// Valida que un checkbox booleano esté marcado como true.
+/// Reemplaza [Range(typeof(bool), "true", "true")] que falla en validación
/// cliente por incompatibilidad entre "True" (.NET) y "true" (HTML/JS).
///
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
@@ -13,7 +13,7 @@ public class DebeSerVerdaderoAttribute : ValidationAttribute, IClientModelValida
{
public DebeSerVerdaderoAttribute()
{
- ErrorMessage = "Debe aceptar los términos";
+ ErrorMessage = "Debe aceptar los términos";
}
public override bool IsValid(object? value)
diff --git a/Ayudantes/FormateadorTiempo.cs b/Ayudantes/FormateadorTiempo.cs
new file mode 100644
index 0000000..5e5035b
--- /dev/null
+++ b/Ayudantes/FormateadorTiempo.cs
@@ -0,0 +1,14 @@
+namespace EpycusApp.Ayudantes
+{
+ public static class FormateadorTiempo
+ {
+ public static string FormatearSegundos(int segundos)
+ {
+ if (segundos < 60) return $"{segundos}s";
+ if (segundos < 3600) return $"{segundos / 60}min";
+ int horas = segundos / 3600;
+ int mins = (segundos % 3600) / 60;
+ return mins > 0 ? $"{horas}h {mins}m" : $"{horas}h";
+ }
+ }
+}
diff --git a/Controllers/AdminController.cs b/Controllers/AdminController.cs
index 1688fb1..ce2260c 100644
--- a/Controllers/AdminController.cs
+++ b/Controllers/AdminController.cs
@@ -120,7 +120,7 @@ public async Task ActivarSuscripcion(int usuarioId)
{
var adminId = ObtenerUsuarioId();
await _servicioAdmin.ActivarSuscripcion(usuarioId, adminId);
- TempData["Exito"] = "Suscripción activada correctamente.";
+ TempData["Exito"] = "Suscripción activada correctamente.";
return RedirectToAction(nameof(Usuarios));
}
@@ -130,7 +130,7 @@ public async Task ActivarSuscripcion(int usuarioId)
public async Task DesactivarSuscripcion(int usuarioId)
{
await _servicioAdmin.DesactivarSuscripcion(usuarioId);
- TempData["Exito"] = "Suscripción desactivada correctamente.";
+ TempData["Exito"] = "Suscripción desactivada correctamente.";
return RedirectToAction(nameof(Usuarios));
}
diff --git a/Controllers/AjustesController.cs b/Controllers/AjustesController.cs
index 1116af6..db1886f 100644
--- a/Controllers/AjustesController.cs
+++ b/Controllers/AjustesController.cs
@@ -1,7 +1,7 @@
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using EpycusApp.Servicios.Interfaces;
+using EpycusApp.Servicios.Interfaces;
using EpycusApp.ViewModels;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace EpycusApp.Controllers;
@@ -18,19 +18,10 @@ public AjustesController(IServicioPerfil servicioPerfil, IServicioAutenticacion
_servicioAutenticacion = servicioAutenticacion;
}
- public async Task Index()
+ [HttpGet]
+ public IActionResult Index()
{
- var usuarioId = ObtenerUsuarioId();
- var perfil = await _servicioPerfil.ObtenerPerfilCompletoAsync(usuarioId);
-
- if (perfil == null)
- {
- return NotFound();
- }
-
- perfil.PersonajesDisponibles = await _servicioPerfil.ObtenerPersonajesDisponiblesAsync(usuarioId);
- ViewBag.Carreras = await _servicioAutenticacion.ObtenerCarrerasActivas();
- return View(perfil);
+ return RedirectToAction("Index", "Perfil");
}
[HttpPost]
@@ -41,7 +32,7 @@ public async Task ActualizarPerfil(ActualizarPerfilViewModel mode
TempData["Error"] = string.Join("; ", ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage));
- return RedirectToAction(nameof(Index));
+ return RedirectToAction("Index", "Perfil");
}
var usuarioId = ObtenerUsuarioId();
@@ -49,14 +40,14 @@ public async Task ActualizarPerfil(ActualizarPerfilViewModel mode
if (resultado.EsExitoso)
{
- TempData["Mensaje"] = "Perfil actualizado correctamente.";
+ TempData["Exito"] = "Perfil actualizado correctamente.";
}
else
{
TempData["Error"] = resultado.Mensaje ?? "No se pudo actualizar el perfil.";
}
- return RedirectToAction(nameof(Index));
+ return RedirectToAction("Index", "Perfil");
}
[HttpPost]
@@ -65,10 +56,9 @@ public async Task CambiarContrasena(CambiarContrasenaViewModel mo
if (!ModelState.IsValid)
{
TempData["Error"] = "Por favor verifica los datos ingresados.";
- return RedirectToAction(nameof(Index));
+ return RedirectToAction("Index", "Perfil");
}
- var usuarioId = ObtenerUsuarioId();
var correo = User.FindFirstValue(ClaimTypes.Email);
var resultado = await _servicioAutenticacion.CambiarContrasenaAsync(
@@ -79,14 +69,14 @@ public async Task CambiarContrasena(CambiarContrasenaViewModel mo
if (resultado.EsExitoso)
{
- TempData["Mensaje"] = "Contraseña cambiada correctamente.";
+ TempData["Exito"] = "Contraseña cambiada correctamente.";
}
else
{
- TempData["Error"] = resultado.Mensaje ?? "No se pudo cambiar la contraseña.";
+ TempData["Error"] = resultado.Mensaje ?? "No se pudo cambiar la contraseña.";
}
- return RedirectToAction(nameof(Index));
+ return RedirectToAction("Index", "Perfil");
}
[HttpPost]
diff --git a/Controllers/Api/ApiAdminController.cs b/Controllers/Api/ApiAdminController.cs
new file mode 100644
index 0000000..6ace7c2
--- /dev/null
+++ b/Controllers/Api/ApiAdminController.cs
@@ -0,0 +1,101 @@
+using EpycusApp.Ayudantes;
+using EpycusApp.DTOs;
+using EpycusApp.Servicios.Interfaces;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
+
+namespace EpycusApp.Controllers.Api
+{
+ [ApiController]
+ [Route("api/v1/admin")]
+ [Authorize]
+ [EnableRateLimiting("Api")]
+ public class ApiAdminController : BaseApiController
+ {
+ private readonly IServicioAdmin _servicioAdmin;
+ private readonly IServicioAutenticacion _servicioAutenticacion;
+
+ public ApiAdminController(IServicioAdmin servicioAdmin, IServicioAutenticacion servicioAutenticacion)
+ {
+ _servicioAdmin = servicioAdmin;
+ _servicioAutenticacion = servicioAutenticacion;
+ }
+
+ [AllowAnonymous]
+ [HttpPost("login")]
+ public async Task Login([FromBody] AdminLoginRequest request)
+ {
+ var (exito, mensaje, token, refreshToken) = await _servicioAutenticacion.Login(request.Correo, request.Contrasena);
+
+ if (!exito)
+ return Ok(RespuestaApi.Fallida(mensaje));
+
+ return Ok(RespuestaApi.Exitosa(new AdminLoginResponseDto { Token = token!, RefreshToken = refreshToken!, Mensaje = mensaje }));
+ }
+
+ [HttpGet("usuarios")]
+ public async Task Usuarios()
+ {
+ var usuarios = await _servicioAdmin.ObtenerTodosUsuarios();
+ return Ok(RespuestaApi