fix: seguridad del webhook de soporte, pagos en contabilidad y Telegram para tareas
- soporte: el webhook de correo entrante era público sin ninguna validación; ahora exige una API key (query ?key= o header) comparada en tiempo constante. Además evita tickets duplicados por reintentos del proveedor (dedup por Message-Id) y enhebra respuestas del mismo remitente en vez de abrir un ticket nuevo por cada correo. - contabilidad: marcar una cuenta por cobrar/pagar como pagada ahora crea y vincula la Transaccion correspondiente (antes el dashboard de ingresos/ egresos nunca reflejaba esos pagos). Se corrige además que actualizar una cuenta por cobrar borraba su transaccion_id en cada PUT. - tareas: se activa por defecto el canal Telegram para tarea_asignada (estaba apagado desde el seed original) y se agrega un flujo real de vinculación de Telegram para el staff interno (código temporal + verificación), igual al que ya existía para los usuarios del portal — sin esto el chat_id de cada usuario había que pegarlo a mano y la notificación nunca llegaba. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1127d944c7
commit
d2bf699b60
@@ -15,11 +15,12 @@ type ProyectoTicket struct {
|
||||
EmailFrom string `json:"email_from" gorm:"column:email_from;size:255"`
|
||||
Titulo string `json:"titulo" gorm:"column:titulo"`
|
||||
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
||||
Estado string `json:"estado" gorm:"column:estado;default:'abierto'"` // abierto|en_progreso|resuelto|cerrado
|
||||
Estado string `json:"estado" gorm:"column:estado;default:'abierto'"` // abierto|en_progreso|resuelto|cerrado
|
||||
Prioridad string `json:"prioridad" gorm:"column:prioridad;default:'media'"` // baja|media|alta|urgente
|
||||
Origen string `json:"origen" gorm:"column:origen;default:'portal'"` // portal|email
|
||||
AsignadoA *uint `json:"asignado_a" gorm:"column:asignado_a;index"`
|
||||
Asignado *Users `json:"asignado" gorm:"foreignKey:AsignadoA"`
|
||||
MessageID string `json:"message_id" gorm:"column:message_id;size:255;index"` // Message-Id del correo que originó el ticket (dedup)
|
||||
Mensajes []TicketMensaje `json:"mensajes" gorm:"foreignKey:TicketID"`
|
||||
}
|
||||
|
||||
@@ -29,11 +30,12 @@ func (ProyectoTicket) TableName() string { return "proyecto_tickets" }
|
||||
|
||||
type TicketMensaje struct {
|
||||
gorm.Model
|
||||
TicketID uint `json:"ticket_id" gorm:"column:ticket_id;index"`
|
||||
Contenido string `json:"contenido" gorm:"column:contenido;type:text"`
|
||||
EsAdmin bool `json:"es_admin" gorm:"column:es_admin;default:false"`
|
||||
AutorNombre string `json:"autor_nombre" gorm:"column:autor_nombre"`
|
||||
LeidoPortal bool `json:"leido_portal" gorm:"column:leido_portal;default:false"`
|
||||
TicketID uint `json:"ticket_id" gorm:"column:ticket_id;index"`
|
||||
Contenido string `json:"contenido" gorm:"column:contenido;type:text"`
|
||||
EsAdmin bool `json:"es_admin" gorm:"column:es_admin;default:false"`
|
||||
AutorNombre string `json:"autor_nombre" gorm:"column:autor_nombre"`
|
||||
LeidoPortal bool `json:"leido_portal" gorm:"column:leido_portal;default:false"`
|
||||
MessageID string `json:"message_id" gorm:"column:message_id;size:255;index"` // Message-Id del correo de respuesta (dedup)
|
||||
}
|
||||
|
||||
func (TicketMensaje) TableName() string { return "ticket_mensajes" }
|
||||
@@ -66,6 +68,34 @@ func CreateTicketMensaje(m *TicketMensaje) error {
|
||||
return app.Http.Database.DB.Create(m).Error
|
||||
}
|
||||
|
||||
// EmailMessageIDYaProcesado indica si un Message-Id de correo ya generó un ticket
|
||||
// o un mensaje de ticket, para no duplicar cuando el proveedor reintenta la entrega.
|
||||
func EmailMessageIDYaProcesado(messageID string) bool {
|
||||
if messageID == "" {
|
||||
return false
|
||||
}
|
||||
var count int64
|
||||
app.Http.Database.DB.Model(&ProyectoTicket{}).Where("message_id = ?", messageID).Count(&count)
|
||||
if count > 0 {
|
||||
return true
|
||||
}
|
||||
app.Http.Database.DB.Model(&TicketMensaje{}).Where("message_id = ?", messageID).Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// GetUltimoTicketAbiertoPorEmail busca el ticket de origen email más reciente y no
|
||||
// cerrado de un remitente, para enhebrar una respuesta en vez de abrir uno nuevo.
|
||||
func GetUltimoTicketAbiertoPorEmail(email string) (*ProyectoTicket, error) {
|
||||
var t ProyectoTicket
|
||||
err := app.Http.Database.DB.
|
||||
Where("email_from = ? AND origen = ? AND estado <> ?", email, "email", "cerrado").
|
||||
Order("created_at DESC").First(&t).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func GetTicketsByPortalUser(portalUserID uint) ([]ProyectoTicket, error) {
|
||||
var items []ProyectoTicket
|
||||
err := app.Http.Database.DB.Where("portal_user_id = ?", portalUserID).
|
||||
@@ -91,7 +121,10 @@ func AssignedToUser(userID uint) error {
|
||||
|
||||
func CountTicketsByEstado() map[string]int64 {
|
||||
result := map[string]int64{}
|
||||
type row struct{ Estado string; Count int64 }
|
||||
type row struct {
|
||||
Estado string
|
||||
Count int64
|
||||
}
|
||||
var rows []row
|
||||
app.Http.Database.DB.Model(&ProyectoTicket{}).
|
||||
Select("estado, count(*) as count").
|
||||
|
||||
Reference in New Issue
Block a user