From b5e6a9285f2fc66ad6cb992b182f1045b8e400ef Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 19 May 2026 13:33:39 -0500 Subject: [PATCH] up --- pkg/models/portal_user.go | 60 ++++++++++++------------- resources/views/portal/proyecto.html | 15 ++++--- rest/controllers/portal_controller.go | 31 ++++++++----- rest/controllers/proyecto_controller.go | 18 +++++++- 4 files changed, 74 insertions(+), 50 deletions(-) diff --git a/pkg/models/portal_user.go b/pkg/models/portal_user.go index 0d38766..bdf51fc 100644 --- a/pkg/models/portal_user.go +++ b/pkg/models/portal_user.go @@ -14,27 +14,27 @@ import ( // - Rol "partner": accede a los proyectos de todos sus ClienteIDs via PortalAccesos. type PortalUser struct { gorm.Model - Nombre string `json:"nombre" gorm:"column:nombre;not null"` - Email string `json:"email" gorm:"column:email;uniqueIndex;not null"` - Password string `json:"-" gorm:"column:password;type:text"` - ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"` // nil si es partner - Cliente *Cliente `json:"cliente" gorm:"foreignKey:ClienteID"` - Rol string `json:"rol" gorm:"column:rol;default:'cliente'"` // cliente|partner - RoleID *uint `json:"role_id" gorm:"column:role_id;index"` - Role *Roles `json:"role" gorm:"foreignKey:RoleID"` - Activo bool `json:"activo" gorm:"column:activo;default:true"` - Notas string `json:"notas" gorm:"column:notas;type:text"` - TelegramChatID string `json:"telegram_chat_id" gorm:"column:telegram_chat_id"` - Telefono string `json:"telefono" gorm:"column:telefono"` - Indicativo string `json:"indicativo" gorm:"column:indicativo"` // ej: +57, +1 - Pais string `json:"pais" gorm:"column:pais"` - Ciudad string `json:"ciudad" gorm:"column:ciudad"` - Documento string `json:"documento" gorm:"column:documento"` // RUT, NIT, CC, etc. - Empresa string `json:"empresa" gorm:"column:empresa"` // empresa que representa - SitioWeb string `json:"sitio_web" gorm:"column:sitio_web"` // sitio web del partner/cliente - DocumentoRutFile string `json:"documento_rut_file" gorm:"column:documento_rut_file"` // ruta del archivo RUT + Nombre string `json:"nombre" gorm:"column:nombre;not null"` + Email string `json:"email" gorm:"column:email;uniqueIndex;not null"` + Password string `json:"-" gorm:"column:password;type:text"` + ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"` // nil si es partner + Cliente *Cliente `json:"cliente" gorm:"foreignKey:ClienteID"` + Rol string `json:"rol" gorm:"column:rol;default:'cliente'"` // cliente|partner + RoleID *uint `json:"role_id" gorm:"column:role_id;index"` + Role *Roles `json:"role" gorm:"foreignKey:RoleID"` + Activo bool `json:"activo" gorm:"column:activo;default:true"` + Notas string `json:"notas" gorm:"column:notas;type:text"` + TelegramChatID string `json:"telegram_chat_id" gorm:"column:telegram_chat_id"` + Telefono string `json:"telefono" gorm:"column:telefono"` + Indicativo string `json:"indicativo" gorm:"column:indicativo"` // ej: +57, +1 + Pais string `json:"pais" gorm:"column:pais"` + Ciudad string `json:"ciudad" gorm:"column:ciudad"` + Documento string `json:"documento" gorm:"column:documento"` // RUT, NIT, CC, etc. + Empresa string `json:"empresa" gorm:"column:empresa"` // empresa que representa + SitioWeb string `json:"sitio_web" gorm:"column:sitio_web"` // sitio web del partner/cliente + DocumentoRutFile string `json:"documento_rut_file" gorm:"column:documento_rut_file"` // ruta del archivo RUT DocumentoRutNombre string `json:"documento_rut_nombre" gorm:"column:documento_rut_nombre"` // nombre original - PortalAccesos []PortalAcceso `json:"portal_accesos" gorm:"foreignKey:PortalUserID"` + PortalAccesos []PortalAcceso `json:"portal_accesos" gorm:"foreignKey:PortalUserID"` } func (PortalUser) TableName() string { return "portal_users" } @@ -87,17 +87,17 @@ func UpdatePortalUser(u *PortalUser) error { "email": u.Email, "cliente_id": u.ClienteID, "rol": u.Rol, - "role_id": u.RoleID, - "activo": u.Activo, - "notas": u.Notas, + "role_id": u.RoleID, + "activo": u.Activo, + "notas": u.Notas, "telegram_chat_id": u.TelegramChatID, - "telefono": u.Telefono, - "indicativo": u.Indicativo, - "pais": u.Pais, - "ciudad": u.Ciudad, - "documento": u.Documento, - "empresa": u.Empresa, - "sitio_web": u.SitioWeb, + "telefono": u.Telefono, + "indicativo": u.Indicativo, + "pais": u.Pais, + "ciudad": u.Ciudad, + "documento": u.Documento, + "empresa": u.Empresa, + "sitio_web": u.SitioWeb, }).Error } diff --git a/resources/views/portal/proyecto.html b/resources/views/portal/proyecto.html index 5dea8e5..dd3af5f 100644 --- a/resources/views/portal/proyecto.html +++ b/resources/views/portal/proyecto.html @@ -152,10 +152,10 @@
- - - + :class="d.tipo==='contrato' ? 'bg-blue-50 text-blue-500' : d.tipo==='orden_servicio' ? 'bg-amber-50 text-amber-500' : 'bg-slate-100 text-slate-400'"> + + +
@@ -163,13 +163,14 @@
- + x-text="(d.tipo||'').replace('_',' ')"> +
+

+ class="flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center text-[#8eb02f] hover:bg-[#8eb02f]/10 transition-colors" title="Descargar">
diff --git a/rest/controllers/portal_controller.go b/rest/controllers/portal_controller.go index ebfd04c..981c203 100644 --- a/rest/controllers/portal_controller.go +++ b/rest/controllers/portal_controller.go @@ -180,14 +180,14 @@ func PortalGetProyectoData(c *fiber.Ctx) error { documentos, _ := models.GetDocumentosByProyecto(proy.ID) return c.JSON(fiber.Map{ - "proyecto": proy, - "fases": fases, - "avances": avances, - "entregables": entregables, - "documentos": documentos, - "tickets": tickets, - "facturas": facturas, - "facturasNoLeidas": facturasNoLeidas, + "proyecto": proy, + "fases": fases, + "avances": avances, + "entregables": entregables, + "documentos": documentos, + "tickets": tickets, + "facturas": facturas, + "facturasNoLeidas": facturasNoLeidas, }) } @@ -252,7 +252,9 @@ func PortalResponderTicket(c *fiber.Ctx) error { return c.Status(401).JSON(fiber.Map{"error": "no autenticado"}) } ticketID, _ := c.ParamsInt("id") - type Req struct{ Contenido string `json:"contenido"` } + type Req struct { + Contenido string `json:"contenido"` + } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) @@ -382,7 +384,7 @@ func PortalDownloadDocumento(c *fiber.Ctx) error { if !hasAccess { return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"}) } - clean := doc.Archivo + clean := filepath.Clean(doc.Archivo) if !strings.HasPrefix(clean, "uploads/") { return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"}) } @@ -390,7 +392,13 @@ func PortalDownloadDocumento(c *fiber.Ctx) error { if nombre == "" { nombre = doc.Nombre } - c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, nombre)) + safe := strings.Map(func(r rune) rune { + if r > 127 || r == '"' || r == '\\' || r == '/' || r == '\n' || r == '\r' { + return '_' + } + return r + }, nombre) + c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, safe, url.PathEscape(nombre))) return c.SendFile(clean) } @@ -792,4 +800,3 @@ func searchTokenInUpdates(botToken, token string) (int64, bool) { } return 0, false } - diff --git a/rest/controllers/proyecto_controller.go b/rest/controllers/proyecto_controller.go index 4f29d5c..3b9f38f 100644 --- a/rest/controllers/proyecto_controller.go +++ b/rest/controllers/proyecto_controller.go @@ -4,6 +4,7 @@ import ( "fmt" "math" "mime/multipart" + "net/url" "os" "path/filepath" "strconv" @@ -491,10 +492,25 @@ func DownloadDocumento(c *fiber.Ctx) error { if !strings.HasPrefix(clean, "uploads/") { return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"}) } - c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, item.OriginalName)) + nombre := item.OriginalName + if nombre == "" { + nombre = item.Nombre + } + c.Set("Content-Disposition", attachmentDisposition(nombre)) return c.SendFile(clean) } +// attachmentDisposition genera un header Content-Disposition con fallback ASCII y encoding RFC 5987. +func attachmentDisposition(name string) string { + safe := strings.Map(func(r rune) rune { + if r > 127 || r == '"' || r == '\\' || r == '/' || r == '\n' || r == '\r' { + return '_' + } + return r + }, name) + return fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, safe, url.PathEscape(name)) +} + // ─── Tickets (admin) ────────────────────────────────────────────────────────── func GetTickets(c *fiber.Ctx) error {