feat: sesión 1 año + validación sintaxis plantilla en tiempo real

This commit is contained in:
Lizandro Guarnizo
2026-04-30 22:53:10 -05:00
parent 46c89457dc
commit 4e564a7e28
7 changed files with 41 additions and 10 deletions
+3 -3
View File
@@ -48,14 +48,14 @@ mail:
token:
app_jwt_secret: SECRET_APP
api_jwt_secret: SECRET_API
expires_in: 3600
expires_in: 31536000
jwt:
app:
secret: SECRET_APP
expire: 3600
expire: 31536000
api:
secret: SECRET_API
expire: 3600
expire: 31536000
template:
path: "resources/view"
+3 -3
View File
@@ -53,14 +53,14 @@ mail:
token:
app_jwt_secret: SECRET_APP
api_jwt_secret: SECRET_API
expires_in: 3600
expires_in: 31536000
jwt:
app:
secret: SECRET_APP
expire: 3600
expire: 31536000
api:
secret: SECRET_API
expire: 3600
expire: 31536000
template:
path: "resources/view"
+3 -1
View File
@@ -28,7 +28,9 @@ func (s *SessionConfig) Setup() error {
}
// Configurar el proveedor de sesiones en memoria
store := session.New() // Usar el proveedor de sesiones en memoria
store := session.New(session.Config{
Expiration: 365 * 24 * time.Hour, // 1 año
}) // Usar el proveedor de sesiones en memoria
// Crear una nueva sesión
s.Session = store
+1 -1
View File
@@ -24,7 +24,7 @@ func (t *Token) CreateToken(c *fiber.Ctx, userID uint, secret string, expire ...
if len(expire) > 0 {
t.Expire = expire[0]
} else {
t.Expire = 3600
t.Expire = 31536000 // 1 año por defecto si no se configura
}
expiresIn := time.Now().Add(time.Duration(t.Expire) * time.Second).Unix()
claims["exp"] = expiresIn
+11 -2
View File
@@ -110,7 +110,9 @@
<label class="text-xs font-medium text-gray-600 block mb-1">HTML del correo *</label>
<textarea x-model="form.cuerpo_html" rows="16"
@input.debounce.600ms="previewInline()"
:class="templateError ? 'border-red-400' : ''"
class="w-full border rounded px-3 py-2 text-xs font-mono" required></textarea>
<p x-show="templateError" x-text="templateError" class="text-red-500 text-xs mt-1"></p>
</div>
<div>
<label class="text-xs font-medium text-gray-600 block mb-1">Vista previa</label>
@@ -177,7 +179,7 @@ document.addEventListener('alpine:init', () => {
loading: false,
datos: [], total: 0, totalPages: 1, page: 1, limit: 10, search: '',
addModal: false, editModal: false, deleteModal: false, previewModal: false, testModal: false,
selectedId: null, testEmail: '',
selectedId: null, testEmail: '', templateError: '',
form: { nombre:'', asunto:'', cuerpo_html:'', tipo:'renovacion' },
toast: { show:false, msg:'', type:'ok' },
@@ -211,18 +213,25 @@ document.addEventListener('alpine:init', () => {
});
},
previewInline() {
async previewInline() {
const frame = this.$refs.previewFrame;
if (frame) frame.srcdoc = this.form.cuerpo_html || '<p style="color:#999;padding:1rem">Sin contenido</p>';
if (!this.form.cuerpo_html) { this.templateError = ''; return; }
try {
const { data } = await axios.post('/app/api/plantillas-correo/validate', { html: this.form.cuerpo_html });
this.templateError = data.ok ? '' : (data.error || 'Sintaxis inválida');
} catch { this.templateError = ''; }
},
closeModals() {
this.addModal = this.editModal = this.deleteModal = this.previewModal = this.testModal = false;
this.selectedId = null;
this.templateError = '';
this.form = { nombre:'', asunto:'', cuerpo_html:'', tipo:'renovacion' };
},
async save() {
if (this.templateError) { this.showToast('Corrige los errores de sintaxis antes de guardar', 'error'); return; }
this.loading = true;
try {
if (this.editModal) {
@@ -4,6 +4,7 @@ import (
"html/template"
"math"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
@@ -101,6 +102,24 @@ func PreviewPlantilla(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"html": html, "ok": true})
}
// ValidarPlantilla comprueba si el CuerpoHTML es un Go template válido
func ValidarPlantilla(c *fiber.Ctx) error {
var body struct {
HTML string `json:"html"`
}
if err := c.BodyParser(&body); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
html := strings.TrimSpace(body.HTML)
if html == "" {
return c.JSON(fiber.Map{"ok": true})
}
if _, err := template.New("validate").Parse(html); err != nil {
return c.Status(400).JSON(fiber.Map{"ok": false, "error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
func TestEnvioPlantilla(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
+1
View File
@@ -42,6 +42,7 @@ func RenovacionesRoutes(protected fiber.Router) {
protected.Delete("/api/plantillas-correo/:id", controllers.DeletePlantilla)
protected.Get("/api/plantillas-correo/:id/preview", controllers.PreviewPlantilla)
protected.Post("/api/plantillas-correo/:id/test", controllers.TestEnvioPlantilla)
protected.Post("/api/plantillas-correo/validate", controllers.ValidarPlantilla)
// ─── Reglas de notificación ───────────────────────────────────────
protected.Get("/reglas-notificacion", middlewares.MenuMiddleware, controllers.ReglasView)