diff --git a/migrations/migrate.go b/migrations/migrate.go index bc6b992..fc6831c 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -81,9 +81,8 @@ func Migrate() { &models.ProyectoEntregable{}, &models.ProyectoTicket{}, &models.TicketMensaje{}, - &models.Factura{}, // Sistema de notificaciones - &models.NotifEventoConfig{}, - &models.SistemaNotificacion{}, ); err != nil { + &models.Factura{}, + ); err != nil { log.Fatalf("Error during main migration: %v", err) } @@ -278,31 +277,6 @@ func SeedPlantillasBase() { log.Println("[SEED] SeedPlantillasBase completado.") } -// MigratePortal crea/actualiza las tablas del módulo Portal de Clientes, Proyectos, -// Facturas y Telegram. Es idempotente. -func MigratePortal() { - db := app.Http.Database.DB - if err := db.AutoMigrate( - &models.Roles{}, // agrega columnas es_portal_cliente / es_portal_partner si no existen - &models.TelegramConfig{}, - &models.TelegramLog{}, - &models.ClienteDocumento{}, - &models.PortalUser{}, - &models.PortalAcceso{}, - &models.Proyecto{}, - &models.ProyectoFase{}, - &models.ProyectoAvance{}, - &models.ProyectoEntregable{}, - &models.ProyectoTicket{}, - &models.TicketMensaje{}, - &models.Factura{}, - ); err != nil { - log.Printf("[MIGRATE] Error en MigratePortal: %v", err) - } else { - log.Println("[MIGRATE] Tablas de Portal OK") - } -} - // MigrateRenovaciones crea/actualiza las tablas del módulo de Renovaciones. // Es idempotente: GORM AutoMigrate solo añade columnas/tablas nuevas, nunca las borra. func MigrateRenovaciones() { @@ -737,7 +711,27 @@ func SeedSaas() { log.Println("[SEED] Seed de SaaS completado.") } -// SeedTelegram agrega el submódulo "Telegram" al módulo "Integraciones". Es idempotente. +// MigratePortal ejecuta AutoMigrate para los modelos del portal de clientes. +func MigratePortal() { + db := app.Http.Database.DB + if err := db.AutoMigrate( + &models.Proyecto{}, + &models.ProyectoFase{}, + &models.ProyectoAvance{}, + &models.ProyectoEntregable{}, + &models.ProyectoTicket{}, + &models.TicketMensaje{}, + &models.PortalUser{}, + &models.PortalAcceso{}, + &models.Factura{}, + ); err != nil { + log.Printf("[MIGRATE] Error en MigratePortal: %v", err) + } else { + log.Println("[MIGRATE] MigratePortal completado.") + } +} + +// SeedTelegram agrega el submódulo de Telegram al módulo "Integraciones". Es idempotente. func SeedTelegram() { db := app.Http.Database.DB @@ -747,160 +741,23 @@ func SeedTelegram() { return } - entries := []struct{ title, desc, url string }{ - {"Telegram", "Notificaciones y alertas por Telegram", "/app/telegram"}, - } - - var insertados []models.Submodules - for _, e := range entries { - var sub models.Submodules - if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { - sub = models.Submodules{ - Title: e.title, - Description: e.desc, - Url: e.url, - ModuleId: modulo.ID, - ModifiedAt: time.Now(), - } - if err := db.Create(&sub).Error; err != nil { - log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) - continue - } - log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) - } else { - log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) - } - insertados = append(insertados, sub) - } - - var roles []models.Roles - if err := db.Find(&roles).Error; err != nil { - log.Printf("[SEED] Error obteniendo roles: %v", err) - return - } - for _, rol := range roles { - if err := db.Model(&rol).Association("Submodules").Append(&insertados); err != nil { - log.Printf("[SEED] Error asignando Telegram al rol '%s': %v", rol.Name, err) - } else { - log.Printf("[SEED] Telegram asignado al rol '%s'", rol.Name) - } - } - log.Println("[SEED] Seed de Telegram completado.") -} - -// SeedPortalClientes crea el módulo "Portal de Clientes" con sus submódulos. Es idempotente. -func SeedPortalClientes() { - db := app.Http.Database.DB - - var modulo models.Modules - if err := db.Where("title = ?", "Portal de Clientes").First(&modulo).Error; err != nil { - modulo = models.Modules{ - Title: "Portal de Clientes", - Description: "Portal de clientes: proyectos, facturas y accesos", - ModifiedAt: time.Now(), - } - if err := db.Create(&modulo).Error; err != nil { - log.Printf("[SEED] Error creando módulo Portal de Clientes: %v", err) - return - } - log.Printf("[SEED] Módulo 'Portal de Clientes' creado con ID %d", modulo.ID) - } else { - log.Printf("[SEED] Módulo 'Portal de Clientes' ya existe (ID %d)", modulo.ID) - } - - entries := []struct{ title, desc, url string }{ - {"Proyectos", "Gestión de proyectos y avances del portal", "/app/proyectos"}, - {"Facturas", "Gestión de facturas y pagos por cliente", "/app/facturas"}, - {"Portal Usuarios", "Usuarios con acceso al portal de clientes", "/app/portal-usuarios"}, - {"Tickets", "Tickets de soporte abiertos por clientes del portal", "/app/tickets"}, - {"Config. Notificaciones", "Configurar canales de notificación por evento", "/app/notif-config"}, - } - - var insertados []models.Submodules - for _, e := range entries { - var sub models.Submodules - if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { - sub = models.Submodules{ - Title: e.title, - Description: e.desc, - Url: e.url, - ModuleId: modulo.ID, - ModifiedAt: time.Now(), - } - if err := db.Create(&sub).Error; err != nil { - log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) - continue - } - log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) - } else { - log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) - } - insertados = append(insertados, sub) - } - - var roles []models.Roles - if err := db.Find(&roles).Error; err != nil { - log.Printf("[SEED] Error obteniendo roles: %v", err) - return - } - for _, rol := range roles { - if err := db.Model(&rol).Association("Submodules").Append(&insertados); err != nil { - log.Printf("[SEED] Error asignando submódulos Portal al rol '%s': %v", rol.Name, err) - } else { - log.Printf("[SEED] Submódulos de Portal de Clientes asignados al rol '%s'", rol.Name) - } - } - log.Println("[SEED] Seed de Portal de Clientes completado.") -} - -// SeedNotifDefaults crea la configuración de notificaciones por defecto. Es idempotente. -func SeedNotifDefaults() { - db := app.Http.Database.DB - defaults := []models.NotifEventoConfig{ - {Evento: "ticket_nuevo", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Admin recibe cuando el cliente abre un ticket"}, - {Evento: "ticket_respuesta_cliente", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Admin recibe cuando el cliente responde un ticket"}, - {Evento: "ticket_respuesta_admin", Destinatario: "portal_user", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Cliente recibe cuando el admin responde su ticket"}, - {Evento: "factura_subida", Destinatario: "portal_user", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Cliente recibe cuando el admin sube el PDF de una factura"}, - } - for _, cfg := range defaults { - var existing models.NotifEventoConfig - if err := db.Where("evento = ? AND destinatario = ?", cfg.Evento, cfg.Destinatario).First(&existing).Error; err != nil { - if err := db.Create(&cfg).Error; err != nil { - log.Printf("[SEED] Error creando notif config %s/%s: %v", cfg.Evento, cfg.Destinatario, err) - } else { - log.Printf("[SEED] Notif config creada: %s → %s", cfg.Evento, cfg.Destinatario) - } - } - } - log.Println("[SEED] Seed de notificaciones completado.") -} - -// SeedShield agrega el submódulo "Shield" al módulo "Integraciones". Es idempotente. -func SeedShield() { - db := app.Http.Database.DB - - var modulo models.Modules - if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil { - log.Printf("[SEED] Módulo 'Integraciones' no encontrado para SeedShield: %v", err) - return - } - + url := "/app/telegram" var sub models.Submodules - if err := db.Where("url = ?", "/app/shield").First(&sub).Error; err != nil { + if err := db.Where("url = ?", url).First(&sub).Error; err != nil { sub = models.Submodules{ - Title: "Shield", - Description: "Administración de la API de seguridad USITE Shield", - Url: "/app/shield", + Title: "Telegram", + Description: "Configuración de bots de Telegram para notificaciones", + Url: url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } - if err := db.Create(&sub).Error; err != nil { - log.Printf("[SEED] Error creando submódulo Shield: %v", err) + if err2 := db.Create(&sub).Error; err2 != nil { + log.Printf("[SEED] Error creando submódulo Telegram: %v", err2) return } - log.Printf("[SEED] Submódulo 'Shield' creado (ID %d)", sub.ID) + log.Printf("[SEED] Submódulo 'Telegram' creado (ID %d)", sub.ID) } else { - log.Printf("[SEED] Submódulo 'Shield' ya existe (ID %d)", sub.ID) + log.Printf("[SEED] Submódulo 'Telegram' ya existe (ID %d)", sub.ID) } var roles []models.Roles @@ -910,5 +767,73 @@ func SeedShield() { for _, rol := range roles { db.Model(&rol).Association("Submodules").Append(&sub) } - log.Println("[SEED] SeedShield completado.") + log.Println("[SEED] Seed de Telegram completado.") +} + +// SeedPortalClientes agrega el módulo "Portal de Clientes" con sus submódulos. Es idempotente. +func SeedPortalClientes() { + db := app.Http.Database.DB + + var modulo models.Modules + if err := db.Where("title = ?", "Portal de Clientes").First(&modulo).Error; err != nil { + modulo = models.Modules{ + Title: "Portal de Clientes", + Description: "Gestión del portal de acceso para clientes", + ModifiedAt: time.Now(), + } + if err2 := db.Create(&modulo).Error; err2 != nil { + log.Printf("[SEED] Error creando módulo 'Portal de Clientes': %v", err2) + return + } + log.Printf("[SEED] Módulo 'Portal de Clientes' creado (ID %d)", modulo.ID) + } else { + log.Printf("[SEED] Módulo 'Portal de Clientes' ya existe (ID %d)", modulo.ID) + } + + entries := []struct{ title, desc, url string }{ + {"Proyectos", "Gestión de proyectos y roadmap para clientes", "/app/proyectos"}, + {"Facturas", "Gestión de facturas y cobros del portal", "/app/facturas"}, + {"Portal Usuarios", "Gestión de usuarios del portal de clientes", "/app/portal-usuarios"}, + } + + var insertados []models.Submodules + for _, e := range entries { + var sub models.Submodules + if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { + sub = models.Submodules{ + Title: e.title, + Description: e.desc, + Url: e.url, + ModuleId: modulo.ID, + ModifiedAt: time.Now(), + } + if err2 := db.Create(&sub).Error; err2 != nil { + log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err2) + continue + } + log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) + } else { + log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) + } + insertados = append(insertados, sub) + } + + var roles []models.Roles + if err := db.Find(&roles).Error; err != nil { + return + } + for _, rol := range roles { + db.Model(&rol).Association("Submodules").Append(&insertados) + } + log.Println("[SEED] Seed de Portal de Clientes completado.") +} + +// SeedNotifDefaults es un stub idempotente para configuraciones de notificaciones por defecto. +func SeedNotifDefaults() { + log.Println("[SEED] SeedNotifDefaults: sin entradas por defecto definidas aún.") +} + +// SeedShield es un stub idempotente para la configuración de Shield. +func SeedShield() { + log.Println("[SEED] SeedShield: sin entradas por defecto definidas aún.") } diff --git a/pkg/auth/portal.go b/pkg/auth/portal.go index 98d2536..906d196 100644 --- a/pkg/auth/portal.go +++ b/pkg/auth/portal.go @@ -2,7 +2,6 @@ package auth import ( "errors" - "log" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/app" @@ -15,20 +14,11 @@ const portalSessionKey = "portal_user_id" func PortalUser(c *fiber.Ctx) (*models.PortalUser, error) { store := app.Http.Session.Get(c) raw := store.Get(portalSessionKey) - log.Printf("[PORTAL SESSION] GET %s → cookie=%q raw=%v", c.Path(), c.Cookies("session_id"), raw) if raw == nil { return nil, errors.New("no portal session") } - var id uint - switch v := raw.(type) { - case uint: - id = v - case int64: - id = uint(v) - case float64: - id = uint(v) - default: - log.Printf("[PORTAL SESSION] type assertion failed: %T", raw) + id, ok := raw.(uint) + if !ok { return nil, errors.New("invalid portal session") } u, err := models.GetPortalUserByID(id) @@ -45,18 +35,8 @@ func PortalUser(c *fiber.Ctx) (*models.PortalUser, error) { // SetPortalSession guarda el portal_user_id en la sesión. func SetPortalSession(c *fiber.Ctx, userID uint) error { store := app.Http.Session.Get(c) - // Regenerar ID de sesión para evitar session fixation - if err := store.Regenerate(); err != nil { - log.Printf("[PORTAL SESSION] Regenerate error: %v", err) - return err - } store.Set(portalSessionKey, userID) - if err := store.Save(); err != nil { - log.Printf("[PORTAL SESSION] Save error: %v", err) - return err - } - log.Printf("[PORTAL SESSION] SET userID=%d cookie=%q", userID, c.Cookies("session_id")) - return nil + return store.Save() } // DestroyPortalSession elimina la sesión del portal. diff --git a/pkg/models/portal_user.go b/pkg/models/portal_user.go index d5613e3..01a8d6f 100644 --- a/pkg/models/portal_user.go +++ b/pkg/models/portal_user.go @@ -14,20 +14,15 @@ 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"` - Cliente *Cliente `json:"cliente" gorm:"foreignKey:ClienteID"` - Rol string `json:"rol" gorm:"column:rol;default:'cliente'"` - 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"` - Pais string `json:"pais" gorm:"column:pais"` - PortalAccesos []PortalAcceso `json:"portal_accesos" gorm:"foreignKey:PortalUserID"` + 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 + Activo bool `json:"activo" gorm:"column:activo;default:true"` + Notas string `json:"notas" gorm:"column:notas;type:text"` + PortalAccesos []PortalAcceso `json:"portal_accesos" gorm:"foreignKey:PortalUserID"` } func (PortalUser) TableName() string { return "portal_users" } @@ -48,7 +43,7 @@ func (PortalAcceso) TableName() string { return "portal_accesos" } func GetAllPortalUsers(limit, offset int) ([]PortalUser, int64, error) { var items []PortalUser var total int64 - db := app.Http.Database.DB.Model(&PortalUser{}).Preload("Cliente").Preload("PortalAccesos.Cliente").Preload("Role") + db := app.Http.Database.DB.Model(&PortalUser{}).Preload("Cliente").Preload("PortalAccesos.Cliente") if err := db.Count(&total).Error; err != nil { return nil, 0, err } @@ -60,7 +55,7 @@ func GetAllPortalUsers(limit, offset int) ([]PortalUser, int64, error) { func GetPortalUserByID(id uint) (*PortalUser, error) { var item PortalUser - err := app.Http.Database.DB.Preload("Cliente").Preload("PortalAccesos.Cliente").Preload("Role").First(&item, id).Error + err := app.Http.Database.DB.Preload("Cliente").Preload("PortalAccesos.Cliente").First(&item, id).Error return &item, err } @@ -80,12 +75,8 @@ 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, - "telegram_chat_id": u.TelegramChatID, - "telefono": u.Telefono, - "pais": u.Pais, + "activo": u.Activo, + "notas": u.Notas, }).Error } @@ -101,6 +92,41 @@ func DeletePortalUser(id uint) error { return app.Http.Database.DB.Delete(&PortalUser{}, id).Error } +// GetPortalUsersByClienteID devuelve todos los portal_users activos asociados a un cliente, +// ya sea directamente (rol cliente) o mediante PortalAcceso (rol partner). +func GetPortalUsersByClienteID(clienteID uint) ([]PortalUser, error) { + var result []PortalUser + db := app.Http.Database.DB + + // Usuarios directos del cliente + var directos []PortalUser + if err := db.Where("cliente_id = ? AND activo = true", clienteID).Find(&directos).Error; err != nil { + return nil, err + } + result = append(result, directos...) + + // Usuarios partner con acceso al cliente via PortalAcceso + var accesos []PortalAcceso + if err := db.Where("cliente_id = ?", clienteID).Find(&accesos).Error; err != nil { + return nil, err + } + seen := make(map[uint]bool) + for _, d := range directos { + seen[d.ID] = true + } + for _, a := range accesos { + if seen[a.PortalUserID] { + continue + } + var u PortalUser + if err := db.Where("id = ? AND activo = true", a.PortalUserID).First(&u).Error; err == nil { + result = append(result, u) + seen[u.ID] = true + } + } + return result, nil +} + func AddPortalAcceso(portalUserID, clienteID uint) error { // Evitar duplicados var existing PortalAcceso @@ -121,11 +147,7 @@ func RemovePortalAcceso(portalUserID, clienteID uint) error { // GetClienteIDsForPortalUser devuelve todos los clienteIDs accesibles para un portal user. func GetClienteIDsForPortalUser(u *PortalUser) []uint { - isPartner := u.Rol == "partner" - if u.Role != nil { - isPartner = u.Role.EsPortalPartner - } - if isPartner { + if u.Rol == "partner" { ids := make([]uint, 0, len(u.PortalAccesos)) for _, a := range u.PortalAccesos { ids = append(ids, a.ClienteID) @@ -153,12 +175,3 @@ func CheckPortalLogin(email, password string) (*PortalUser, error) { } return u, nil } - -// GetPortalUsersByClienteID devuelve todos los portal users activos de un cliente. -func GetPortalUsersByClienteID(clienteID uint) ([]PortalUser, error) { - var users []PortalUser - err := app.Http.Database.DB. - Where("cliente_id = ? AND activo = true", clienteID). - Find(&users).Error - return users, err -} diff --git a/pkg/models/proyecto.go b/pkg/models/proyecto.go index 4afb518..2e57fca 100644 --- a/pkg/models/proyecto.go +++ b/pkg/models/proyecto.go @@ -124,11 +124,8 @@ func GetAllProyectos(limit, offset int, search string) ([]Proyecto, int64, error } func GetProyectosByClienteIDs(clienteIDs []uint) ([]Proyecto, error) { - if len(clienteIDs) == 0 { - return []Proyecto{}, nil - } var items []Proyecto - err := app.Http.Database.DB.Preload("Cliente").Where("cliente_id IN ?", clienteIDs).Order("created_at DESC").Find(&items).Error + err := app.Http.Database.DB.Where("cliente_id IN ?", clienteIDs).Order("created_at DESC").Find(&items).Error return items, err } diff --git a/pkg/models/proyecto_ticket.go b/pkg/models/proyecto_ticket.go index ac0dce9..45bdbd2 100644 --- a/pkg/models/proyecto_ticket.go +++ b/pkg/models/proyecto_ticket.go @@ -9,8 +9,7 @@ import ( type ProyectoTicket struct { gorm.Model - ProyectoID uint `json:"proyecto_id" gorm:"column:proyecto_id;index"` - Proyecto Proyecto `json:"proyecto" gorm:"foreignKey:ProyectoID"` + ProyectoID uint `json:"proyecto_id" gorm:"column:proyecto_id;index"` PortalUserID uint `json:"portal_user_id" gorm:"column:portal_user_id;index"` AutorNombre string `json:"autor_nombre" gorm:"column:autor_nombre"` Titulo string `json:"titulo" gorm:"column:titulo"` @@ -68,13 +67,3 @@ func GetTicketsByPortalUser(portalUserID uint) ([]ProyectoTicket, error) { Preload("Mensajes").Order("created_at DESC").Find(&items).Error return items, err } - -func GetAllTickets(estado string) ([]ProyectoTicket, error) { - var items []ProyectoTicket - db := app.Http.Database.DB.Preload("Proyecto").Preload("Mensajes").Order("created_at DESC") - if estado != "" && estado != "todos" { - db = db.Where("estado = ?", estado) - } - err := db.Find(&items).Error - return items, err -} diff --git a/pkg/services/notif_dispatch.go b/pkg/services/notif_dispatch.go index 6ba156a..933ed6b 100644 --- a/pkg/services/notif_dispatch.go +++ b/pkg/services/notif_dispatch.go @@ -8,118 +8,61 @@ import ( ) // ─── DispatchTicketNuevo ────────────────────────────────────────────────────── -// Llamar cuando el portal user crea un nuevo ticket. -// Notifica al ADMIN según su configuración de canales. +// Notifica al admin cuando un portal user crea un ticket. func DispatchTicketNuevo(ticket *models.ProyectoTicket, portalUser *models.PortalUser, proyectoNombre string) { - cfg := models.GetNotifConfig("ticket_nuevo", "admin") - if cfg == nil { + if ticket == nil || portalUser == nil { return } - ticketURL := "/app/tickets" - titulo := fmt.Sprintf("Nuevo ticket: %s", ticket.Titulo) - cuerpo := fmt.Sprintf("Cliente %s abrió: %s", ticket.AutorNombre, ticket.Titulo) - - if cfg.CanalSistema { - _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ - TipoUsuario: "admin", - UsuarioID: 0, - Titulo: titulo, - Cuerpo: cuerpo, - Url: ticketURL, - Icono: "🎫", - }) - } - if cfg.CanalEmail { - if adminEmail := getAdminEmail(); adminEmail != "" { - SendTicketNuevoAdmin(adminEmail, proyectoNombre, ticket.AutorNombre, ticket.Titulo, ticket.Descripcion, ticketURL) - } - } - if cfg.CanalTelegram { - msg := fmt.Sprintf("🎫 Nuevo ticket\nProyecto: %s\nCliente: %s\nTítulo: %s\n\n%s\n\n🔗 %s", - proyectoNombre, ticket.AutorNombre, ticket.Titulo, ticket.Descripcion, ticketURL) - sendTelegramAdmin(msg) - } + msg := fmt.Sprintf("🎫 Nuevo ticket\nProyecto: %s\nCliente: %s\nTítulo: %s\n\n%s", + proyectoNombre, ticket.AutorNombre, ticket.Titulo, ticket.Descripcion) + sendTelegramAdmin(msg) } // ─── DispatchTicketRespuestaCliente ────────────────────────────────────────── -// Llamar cuando el portal user responde un ticket. -// Notifica al ADMIN según su configuración de canales. +// Notifica al admin cuando el portal user responde un ticket. func DispatchTicketRespuestaCliente(ticket *models.ProyectoTicket, contenido string, proyectoNombre string) { - cfg := models.GetNotifConfig("ticket_respuesta_cliente", "admin") - if cfg == nil { + if ticket == nil { return } - ticketURL := "/app/tickets" - titulo := fmt.Sprintf("Respuesta de cliente: %s", ticket.Titulo) - cuerpo := fmt.Sprintf("%s respondió: %s", ticket.AutorNombre, contenido) - - if cfg.CanalSistema { - _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ - TipoUsuario: "admin", - UsuarioID: 0, - Titulo: titulo, - Cuerpo: cuerpo, - Url: ticketURL, - Icono: "💬", - }) - } - if cfg.CanalEmail { - if adminEmail := getAdminEmail(); adminEmail != "" { - SendTicketRespuestaAdmin(adminEmail, proyectoNombre, ticket.AutorNombre, ticket.Titulo, contenido, ticketURL) - } - } - if cfg.CanalTelegram { - msg := fmt.Sprintf("💬 Respuesta de cliente\nProyecto: %s\nCliente: %s\n\n%s\n\n🔗 %s", - proyectoNombre, ticket.AutorNombre, contenido, ticketURL) - sendTelegramAdmin(msg) - } + msg := fmt.Sprintf("💬 Respuesta de cliente\nProyecto: %s\nCliente: %s\n\n%s", + proyectoNombre, ticket.AutorNombre, contenido) + sendTelegramAdmin(msg) } // ─── DispatchTicketRespuestaAdmin ───────────────────────────────────────────── -// Llamar cuando el admin responde un ticket. -// Notifica al PORTAL USER según su configuración de canales. +// Notifica al portal user cuando el admin responde un ticket. func DispatchTicketRespuestaAdmin(ticket *models.ProyectoTicket, contenido string, proyectoNombre string) { - cfg := models.GetNotifConfig("ticket_respuesta_admin", "portal_user") - if cfg == nil { + if ticket == nil { return } - // Obtener portal user para saber email y telegram portalUser, err := models.GetPortalUserByID(ticket.PortalUserID) if err != nil || portalUser == nil { log.Printf("[Notif] PortalUser %d no encontrado: %v", ticket.PortalUserID, err) return } - - portalURL := fmt.Sprintf("/portal/proyecto/%s", ticket.Proyecto.Slug) - titulo := fmt.Sprintf("Respuesta en tu ticket: %s", ticket.Titulo) - cuerpo := fmt.Sprintf("El equipo respondió: %s", contenido) - - if cfg.CanalSistema { - _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ - TipoUsuario: "portal_user", - UsuarioID: portalUser.ID, - Titulo: titulo, - Cuerpo: cuerpo, - Url: portalURL, - Icono: "💬", - }) - } - if cfg.CanalEmail && portalUser.Email != "" { - SendTicketRespuestaPortalUser(portalUser.Email, portalUser.Nombre, proyectoNombre, ticket.Titulo, contenido, portalURL) - } - if cfg.CanalTelegram && portalUser.TelegramChatID != "" { - ts := NewTelegramService() - msg := fmt.Sprintf("💬 El equipo respondió tu ticket\nProyecto: %s\nTicket: %s\n\n%s\n\n🔗 %s", - proyectoNombre, ticket.Titulo, contenido, portalURL) - if err := ts.SendMessageWithToken(portalUser.TelegramChatID, msg, getAdminBotToken()); err != nil { - log.Printf("[Notif] Error telegram portal_user %d: %v", portalUser.ID, err) - } + if portalUser.Email == "" { + return } + // TODO: enviar email al portal user cuando mail_service implemente SendTicketRespuestaPortalUser + _ = fmt.Sprintf("/portal/dashboard") } -// ─── helpers internos ───────────────────────────────────────────────────────── +// ─── DispatchFacturaSubida ──────────────────────────────────────────────────── +// Notifica a los portal_users del cliente cuando se sube una factura. +func DispatchFacturaSubida(factura *models.Factura) { + if factura == nil || factura.ClienteID == 0 { + return + } + portalUsers, err := models.GetPortalUsersByClienteID(factura.ClienteID) + if err != nil || len(portalUsers) == 0 { + return + } + log.Printf("[Notif] DispatchFacturaSubida factura=%d portal_users=%d (pendiente de implementar)", + factura.ID, len(portalUsers)) +} + +// ─── helpers ───────────────────────────────────────────────────────────────── -// getAdminEmail devuelve el from_address del SMTP activo como email del admin. func getAdminEmail() string { cfg, err := models.GetSmtpConfig() if err != nil || cfg == nil { @@ -128,7 +71,6 @@ func getAdminEmail() string { return cfg.FromAddress } -// getAdminBotToken devuelve el token del primer TelegramConfig activo. func getAdminBotToken() string { configs, err := models.GetAllTelegramConfigs() if err != nil { @@ -142,7 +84,6 @@ func getAdminBotToken() string { return "" } -// sendTelegramAdmin envía un mensaje usando el primer TelegramConfig activo. func sendTelegramAdmin(mensaje string) { configs, err := models.GetAllTelegramConfigs() if err != nil { @@ -159,56 +100,3 @@ func sendTelegramAdmin(mensaje string) { } } } -// ─── DispatchFacturaSubida ──────────────────────────────────────────────────── -// Llamar cuando el admin sube el PDF de una factura. -// Notifica a todos los portal_users activos del cliente. -func DispatchFacturaSubida(factura *models.Factura) { - cfg := models.GetNotifConfig("factura_subida", "portal_user") - if cfg == nil { - return - } - if factura.ClienteID == 0 { - return - } - portalUsers, err := models.GetPortalUsersByClienteID(factura.ClienteID) - if err != nil || len(portalUsers) == 0 { - return - } - - clienteNombre := "" - if factura.Cliente.Nombre != "" { - clienteNombre = factura.Cliente.Nombre - } else if factura.Cliente.Empresa != "" { - clienteNombre = factura.Cliente.Empresa - } - - for _, u := range portalUsers { - u := u // capture - portalURL := "/portal/dashboard" - titulo := fmt.Sprintf("Nueva factura disponible: %s", factura.Numero) - cuerpo := fmt.Sprintf("Factura %s por %s %s", factura.Numero, factura.Moneda, fmt.Sprintf("%.2f", factura.Monto)) - - if cfg.CanalSistema { - _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ - TipoUsuario: "portal_user", - UsuarioID: u.ID, - Titulo: titulo, - Cuerpo: cuerpo, - Url: portalURL, - Icono: "🧾", - }) - } - if cfg.CanalEmail && u.Email != "" { - go SendFacturaSubidaPortalUser(u.Email, u.Nombre, clienteNombre, factura.Numero, factura.Monto, factura.Moneda, portalURL) - } - if cfg.CanalTelegram && u.TelegramChatID != "" { - ts := NewTelegramService() - msg := fmt.Sprintf("🧾 Nueva factura disponible\nNúmero: %s\nMonto: %s %.2f\n\n🔗 %s", - factura.Numero, factura.Moneda, factura.Monto, portalURL) - if err := ts.SendMessageWithToken(u.TelegramChatID, msg, getAdminBotToken()); err != nil { - log.Printf("[Notif] Error telegram portal_user %d: %v", u.ID, err) - } - } - } - log.Printf("[Notif] DispatchFacturaSubida factura=%d clientes notificados=%d", factura.ID, len(portalUsers)) -} \ No newline at end of file diff --git a/resources/views/layouts/portal.html b/resources/views/layouts/portal.html index 2437d40..7a6e2db 100644 --- a/resources/views/layouts/portal.html +++ b/resources/views/layouts/portal.html @@ -20,84 +20,15 @@
-Accesos del portal de clientes
@@ -11,15 +11,6 @@| - + |
-
+
-
+
@@ -67,9 +58,6 @@
-
@@ -94,16 +82,12 @@
-
-
+
-
-
-
-
diff --git a/resources/views/proyecto_detalle.html b/resources/views/proyecto_detalle.html
index 48a2349..6944e1d 100644
--- a/resources/views/proyecto_detalle.html
+++ b/resources/views/proyecto_detalle.html
@@ -8,10 +8,10 @@
-
- Enviar credenciales-Se enviará el correo con los datos de acceso al portal a . -
-
-
-
-
-
-
-
-
- {{ .proyecto.Nombre }}-{{ if .proyecto.Cliente }}{{ .proyecto.Cliente.RazonSocial }}{{ end }} · slug: {{ if .proyecto.Cliente.Empresa }}{{ .proyecto.Cliente.Empresa }}{{ else if .proyecto.Cliente.Nombre }}{{ .proyecto.Cliente.Nombre }}{{ end }} · slug:
- {{ .proyecto.Estado }}
+ {{ if eq .proyecto.Estado "activo" }}Activo{{ else if eq .proyecto.Estado "pausado" }}Pausado{{ else if eq .proyecto.Estado "completado" }}Completado{{ else }}{{ .proyecto.Estado }}{{ end }}
{{ .proyecto.Progreso }}% completado
diff --git a/rest/controllers/factura_controller.go b/rest/controllers/factura_controller.go
index ecbdeeb..ab08d70 100644
--- a/rest/controllers/factura_controller.go
+++ b/rest/controllers/factura_controller.go
@@ -11,7 +11,6 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
- "github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
func FacturasIndex(c *fiber.Ctx) error {
@@ -186,10 +185,6 @@ func UploadFacturaPDF(c *fiber.Ctx) error {
if err := models.UpdateFacturaArchivo(uint(id), savePath, file.Filename); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
- // Notificar al cliente
- if f, err := models.GetFacturaByID(uint(id)); err == nil {
- go services.DispatchFacturaSubida(f)
- }
return c.JSON(fiber.Map{"ok": true, "archivo": savePath})
}
diff --git a/rest/controllers/portal_controller.go b/rest/controllers/portal_controller.go
index 3c3473d..82251f7 100644
--- a/rest/controllers/portal_controller.go
+++ b/rest/controllers/portal_controller.go
@@ -2,23 +2,19 @@ package controllers
import (
"fmt"
- "log"
"net/url"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/auth"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
- "github.com/sujit-baniya/fiber-boilerplate/pkg/services"
"github.com/sujit-baniya/fiber-boilerplate/rest/middlewares"
)
// ─── Auth ─────────────────────────────────────────────────────────────────────
func PortalLoginPage(c *fiber.Ctx) error {
- // Usar PortalUser (validación completa) en vez de IsPortalLoggedIn (solo key)
- // para evitar loops cuando hay sesión obsoleta pero el usuario ya no existe en DB.
- if user, err := auth.PortalUser(c); err == nil && user != nil {
+ if auth.IsPortalLoggedIn(c) {
return c.Redirect("/portal/dashboard")
}
return c.Render("portal/login", fiber.Map{
@@ -31,14 +27,11 @@ func PortalLoginPost(c *fiber.Ctx) error {
password := c.FormValue("password")
u, err := models.CheckPortalLogin(email, password)
if err != nil {
- log.Printf("[PORTAL LOGIN] Fallo para %q: %v", email, err)
return c.Redirect("/portal/login?error=" + url.QueryEscape(err.Error()))
}
if err := auth.SetPortalSession(c, u.ID); err != nil {
- log.Printf("[PORTAL LOGIN] Error guardando sesión para usuario %d: %v", u.ID, err)
return c.Redirect("/portal/login?error=Error+interno")
}
- log.Printf("[PORTAL LOGIN] OK usuario %d (%s)", u.ID, email)
return c.Redirect("/portal/dashboard")
}
@@ -52,14 +45,12 @@ func PortalLogout(c *fiber.Ctx) error {
func PortalDashboard(c *fiber.Ctx) error {
u := middlewares.PortalUserFromLocals(c)
if u == nil {
- log.Println("[PORTAL DASHBOARD] Usuario no en locals, redirigiendo a login")
return c.Redirect("/portal/login")
}
// Recargar con accesos
fullUser, err := models.GetPortalUserByID(u.ID)
if err != nil {
- log.Printf("[PORTAL DASHBOARD] Error cargando usuario %d: %v", u.ID, err)
return c.Redirect("/portal/login")
}
@@ -87,7 +78,7 @@ func PortalDashboard(c *fiber.Ctx) error {
"portalUser": fullUser,
"proyectos": proyectos,
"grupos": grupos,
- "isPartner": fullUser.Rol == "partner" || (fullUser.Role != nil && fullUser.Role.EsPortalPartner),
+ "isPartner": fullUser.Rol == "partner",
}, "layouts/portal")
}
@@ -229,7 +220,6 @@ func PortalCrearTicket(c *fiber.Ctx) error {
if err := models.CreateProyectoTicket(t); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
- go services.DispatchTicketNuevo(t, u, proy.Nombre)
return c.Status(201).JSON(t)
}
@@ -264,7 +254,6 @@ func PortalResponderTicket(c *fiber.Ctx) error {
if err := models.CreateTicketMensaje(msg); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
- go services.DispatchTicketRespuestaCliente(ticket, req.Contenido, ticket.Proyecto.Nombre)
return c.Status(201).JSON(msg)
}
@@ -335,42 +324,3 @@ func PortalDownloadFactura(c *fiber.Ctx) error {
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, f.OriginalName))
return c.SendFile(clean)
}
-
-// ─── Notificaciones in-app del portal user ────────────────────────────────────
-
-func PortalGetNotifs(c *fiber.Ctx) error {
- u := middlewares.PortalUserFromLocals(c)
- if u == nil {
- return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
- }
- soloNoLeidas := c.Query("no_leidas") == "1"
- items, err := models.GetSistemaNotifs("portal_user", u.ID, soloNoLeidas)
- if err != nil {
- return c.Status(500).JSON(fiber.Map{"error": err.Error()})
- }
- count := models.CountUnreadNotifs("portal_user", u.ID)
- return c.JSON(fiber.Map{"items": items, "unread": count})
-}
-
-func PortalMarcarNotifLeida(c *fiber.Ctx) error {
- u := middlewares.PortalUserFromLocals(c)
- if u == nil {
- return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
- }
- id, _ := c.ParamsInt("id")
- if err := models.MarcarNotifLeida(uint(id)); err != nil {
- return c.Status(500).JSON(fiber.Map{"error": err.Error()})
- }
- return c.JSON(fiber.Map{"ok": true})
-}
-
-func PortalMarcarTodasLeidas(c *fiber.Ctx) error {
- u := middlewares.PortalUserFromLocals(c)
- if u == nil {
- return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
- }
- if err := models.MarcarTodasLeidas("portal_user", u.ID); err != nil {
- return c.Status(500).JSON(fiber.Map{"error": err.Error()})
- }
- return c.JSON(fiber.Map{"ok": true})
-}
diff --git a/rest/controllers/portal_usuario_controller.go b/rest/controllers/portal_usuario_controller.go
index c2634e0..3d99388 100644
--- a/rest/controllers/portal_usuario_controller.go
+++ b/rest/controllers/portal_usuario_controller.go
@@ -8,7 +8,6 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/app"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
- "github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
func PortalUsuariosIndex(c *fiber.Ctx) error {
@@ -30,14 +29,12 @@ func LoadPortalUsuarios(c *fiber.Ctx) error {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
clientes, _, _ := models.GetAllClientes(200, 0, "")
- portalRoles, _ := models.GetPortalRoles()
return c.JSON(fiber.Map{
- "items": items,
- "total": total,
- "totalPages": int(math.Ceil(float64(total) / float64(limit))),
- "page": page,
- "clientes": clientes,
- "portalRoles": portalRoles,
+ "items": items,
+ "total": total,
+ "totalPages": int(math.Ceil(float64(total) / float64(limit))),
+ "page": page,
+ "clientes": clientes,
})
}
@@ -47,7 +44,7 @@ func CreatePortalUsuario(c *fiber.Ctx) error {
Email string `json:"email"`
Password string `json:"password"`
ClienteID *uint `json:"cliente_id"`
- RoleID *uint `json:"role_id"`
+ Rol string `json:"rol"`
Notas string `json:"notas"`
}
var req Req
@@ -61,23 +58,15 @@ func CreatePortalUsuario(c *fiber.Ctx) error {
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Error al hashear contraseña"})
}
- // Derivar Rol del tipo de rol seleccionado
- rol := "cliente"
- if req.RoleID != nil {
- var role models.Roles
- if err := app.Http.Database.DB.First(&role, *req.RoleID).Error; err == nil {
- if role.EsPortalPartner {
- rol = "partner"
- }
- }
+ if req.Rol == "" {
+ req.Rol = "cliente"
}
u := &models.PortalUser{
Nombre: req.Nombre,
Email: strings.ToLower(strings.TrimSpace(req.Email)),
Password: hashed,
ClienteID: req.ClienteID,
- RoleID: req.RoleID,
- Rol: rol,
+ Rol: req.Rol,
Activo: true,
Notas: req.Notas,
}
@@ -98,7 +87,7 @@ func UpdatePortalUsuario(c *fiber.Ctx) error {
Email string `json:"email"`
Password string `json:"password"`
ClienteID *uint `json:"cliente_id"`
- RoleID *uint `json:"role_id"`
+ Rol string `json:"rol"`
Activo bool `json:"activo"`
Notas string `json:"notas"`
}
@@ -106,22 +95,11 @@ func UpdatePortalUsuario(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
- // Derivar Rol del tipo de rol seleccionado
- rol := "cliente"
- if req.RoleID != nil {
- var role models.Roles
- if err := app.Http.Database.DB.First(&role, *req.RoleID).Error; err == nil {
- if role.EsPortalPartner {
- rol = "partner"
- }
- }
- }
u := &models.PortalUser{
Nombre: req.Nombre,
Email: strings.ToLower(strings.TrimSpace(req.Email)),
ClienteID: req.ClienteID,
- RoleID: req.RoleID,
- Rol: rol,
+ Rol: req.Rol,
Activo: req.Activo,
Notas: req.Notas,
}
@@ -131,7 +109,7 @@ func UpdatePortalUsuario(c *fiber.Ctx) error {
}
// Actualizar password solo si se envió
if strings.TrimSpace(req.Password) != "" {
- hashed, err := app.Http.Hash.Create(req.Password)
+ hashed, err := app.Http.Hash.Create(req.Password)
if err == nil {
_ = models.UpdatePortalUserPassword(uint(id), hashed)
}
@@ -173,29 +151,3 @@ func RemovePortalAcceso(c *fiber.Ctx) error {
}
return c.JSON(fiber.Map{"ok": true})
}
-
-// SendPortalCredentials envía las credenciales de acceso al portal al correo del usuario.
-func SendPortalCredentials(c *fiber.Ctx) error {
- id, err := strconv.ParseUint(c.Params("id"), 10, 32)
- if err != nil {
- return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
- }
- u, err := models.GetPortalUserByID(uint(id))
- if err != nil {
- return c.Status(404).JSON(fiber.Map{"error": "Usuario no encontrado"})
- }
-
- type Req struct {
- Password string `json:"password"`
- }
- var req Req
- _ = c.BodyParser(&req)
-
- password := req.Password
- if password == "" {
- password = "(la contraseña que configuraste)"
- }
-
- services.SendPortalCredentialsEmail(u.Email, u.Nombre, password)
- return c.JSON(fiber.Map{"ok": true, "message": "Correo enviado a " + u.Email})
-}
diff --git a/rest/controllers/proyecto_controller.go b/rest/controllers/proyecto_controller.go
index 1aaa20a..994ceb1 100644
--- a/rest/controllers/proyecto_controller.go
+++ b/rest/controllers/proyecto_controller.go
@@ -12,7 +12,6 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
- "github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
// ─── Admin pages ──────────────────────────────────────────────────────────────
@@ -67,7 +66,7 @@ func LoadProyectos(c *fiber.Ctx) error {
func CreateProyecto(c *fiber.Ctx) error {
type Req struct {
- ClienteID string `json:"cliente_id"`
+ ClienteID uint `json:"cliente_id"`
Nombre string `json:"nombre"`
Slug string `json:"slug"`
Descripcion string `json:"descripcion"`
@@ -78,8 +77,7 @@ func CreateProyecto(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
- clienteID, _ := strconv.ParseUint(req.ClienteID, 10, 64)
- if clienteID == 0 || strings.TrimSpace(req.Nombre) == "" {
+ if req.ClienteID == 0 || strings.TrimSpace(req.Nombre) == "" {
return c.Status(400).JSON(fiber.Map{"error": "cliente y nombre son requeridos"})
}
if req.Slug == "" {
@@ -92,7 +90,7 @@ func CreateProyecto(c *fiber.Ctx) error {
req.Estado = "activo"
}
p := &models.Proyecto{
- ClienteID: uint(clienteID),
+ ClienteID: req.ClienteID,
Nombre: req.Nombre,
Slug: req.Slug,
Descripcion: req.Descripcion,
@@ -111,7 +109,7 @@ func UpdateProyecto(c *fiber.Ctx) error {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
type Req struct {
- ClienteID string `json:"cliente_id"`
+ ClienteID uint `json:"cliente_id"`
Nombre string `json:"nombre"`
Slug string `json:"slug"`
Descripcion string `json:"descripcion"`
@@ -123,9 +121,8 @@ func UpdateProyecto(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
- updateClienteID, _ := strconv.ParseUint(req.ClienteID, 10, 64)
p := &models.Proyecto{
- ClienteID: uint(updateClienteID),
+ ClienteID: req.ClienteID,
Nombre: req.Nombre,
Slug: req.Slug,
Descripcion: req.Descripcion,
@@ -459,11 +456,6 @@ func AdminResponderTicket(c *fiber.Ctx) error {
}
// Cambiar estado a en_progreso si estaba abierto
_ = models.UpdateTicketEstado(uint(ticketID), "en_progreso")
- // Notificar al portal user
- ticket, _ := models.GetTicketByID(uint(ticketID))
- if ticket != nil {
- go services.DispatchTicketRespuestaAdmin(ticket, req.Contenido, ticket.Proyecto.Nombre)
- }
return c.Status(201).JSON(msg)
}
@@ -506,21 +498,3 @@ func mimeFromHeader(fh *multipart.FileHeader) string {
}
return ct
}
-
-// ─── Tickets global (admin) ───────────────────────────────────────────────────
-
-func TicketsAdminIndex(c *fiber.Ctx) error {
- return c.Render("tickets_admin", fiber.Map{
- "user": c.Locals("user"),
- "modules": c.Locals("userModules"),
- }, "layouts/main")
-}
-
-func GetAllTicketsAdmin(c *fiber.Ctx) error {
- estado := c.Query("estado", "todos")
- items, err := models.GetAllTickets(estado)
- if err != nil {
- return c.Status(500).JSON(fiber.Map{"error": err.Error()})
- }
- return c.JSON(items)
-}
diff --git a/rest/middlewares/portal_auth.go b/rest/middlewares/portal_auth.go
index bf41fc4..696455e 100644
--- a/rest/middlewares/portal_auth.go
+++ b/rest/middlewares/portal_auth.go
@@ -7,13 +7,11 @@ import (
)
// PortalAuth protege las rutas del portal de clientes.
-// Si no hay sesión válida, destruye la sesión obsoleta y redirige a /portal/login.
+// Si no hay sesión activa, redirige a /portal/login.
func PortalAuth() fiber.Handler {
return func(c *fiber.Ctx) error {
user, err := auth.PortalUser(c)
if err != nil || user == nil {
- // Limpiar sesión inválida/obsoleta para romper posibles redirect loops
- _ = auth.DestroyPortalSession(c)
return c.Redirect("/portal/login")
}
c.Locals("portalUser", user)
diff --git a/rest/routes/portal.go b/rest/routes/portal.go
index 4f4afac..81829dc 100644
--- a/rest/routes/portal.go
+++ b/rest/routes/portal.go
@@ -29,9 +29,4 @@ func PortalRoutes(app fiber.Router) {
// Descargas
portal.Get("/entregables/:id/download", controllers.PortalDownloadEntregable)
portal.Get("/facturas/:id/download", controllers.PortalDownloadFactura)
-
- // Notificaciones in-app del portal user
- portal.Get("/mis-notifs", controllers.PortalGetNotifs)
- portal.Put("/mis-notifs/:id/leida", controllers.PortalMarcarNotifLeida)
- portal.Post("/mis-notifs/marcar-todas", controllers.PortalMarcarTodasLeidas)
}
diff --git a/rest/routes/user.go b/rest/routes/user.go
index 042bcef..b3b8a0b 100755
--- a/rest/routes/user.go
+++ b/rest/routes/user.go
@@ -154,26 +154,6 @@ func UserRoutes(app fiber.Router) {
protected.Get("/cloudflare/zones/:zone_id/ssl", controllers.GetCloudflareSSL)
protected.Get("/cloudflare/zones/:zone_id/firewall", controllers.GetCloudflareFirewall)
- // ─── USITE Shield ─────────────────────────────────────────────────────────
- protected.Get("/shield", middlewares.MenuMiddleware, controllers.ShieldIndex)
- protected.Get("/loadshield", controllers.LoadShieldConfig)
- protected.Post("/saveshield", controllers.SaveShieldConfig)
- protected.Get("/shield/health", controllers.ShieldHealth)
- protected.Get("/shield/extension-version", controllers.ShieldExtensionVersion)
- protected.Get("/shield/logs", controllers.ShieldLogs)
- protected.Get("/shield/logs/stats", controllers.ShieldLogStats)
- protected.Get("/shield/review-requests", controllers.ShieldReviewRequests)
- protected.Put("/shield/review-requests/:id/approve", controllers.ShieldApproveRequest)
- protected.Put("/shield/review-requests/:id/reject", controllers.ShieldRejectRequest)
- protected.Get("/shield/whitelist", controllers.ShieldWhitelist)
- protected.Post("/shield/whitelist", controllers.ShieldAddWhitelist)
- protected.Delete("/shield/whitelist/:domain", controllers.ShieldDeleteWhitelist)
- protected.Get("/shield/blacklist", controllers.ShieldBlacklist)
- protected.Post("/shield/blacklist", controllers.ShieldAddBlacklist)
- protected.Delete("/shield/blacklist/:domain", controllers.ShieldDeleteBlacklist)
- protected.Get("/shield/reputation/:domain", controllers.ShieldReputation)
- protected.Put("/shield/reputation/:domain/score", controllers.ShieldAdjustScore)
-
// ─── Pasarelas de Pago ────────────────────────────────────────────
protected.Get("/pasarelas-pago", middlewares.MenuMiddleware, controllers.PasarelasPage)
// Bold
@@ -267,22 +247,6 @@ func UserRoutes(app fiber.Router) {
protected.Put("/proyectos/:id/tickets/:ticketID/estado", controllers.UpdateTicketEstadoAdmin)
protected.Post("/proyectos/:id/tickets/:ticketID/mensaje", controllers.AdminResponderTicket)
- // Tickets global (todos los proyectos)
- protected.Get("/tickets", middlewares.MenuMiddleware, controllers.TicketsAdminIndex)
- protected.Get("/tickets/data", controllers.GetAllTicketsAdmin)
- protected.Put("/tickets/:ticketID/estado", controllers.UpdateTicketEstadoAdmin)
- protected.Post("/tickets/:ticketID/mensaje", controllers.AdminResponderTicket)
-
- // Configuración de notificaciones
- protected.Get("/notif-config", middlewares.MenuMiddleware, controllers.NotifConfigIndex)
- protected.Get("/notif-config/data", controllers.GetNotifConfigs)
- protected.Post("/notif-config", controllers.SaveNotifConfig)
-
- // Notificaciones in-app del admin (bell)
- protected.Get("/mis-notifs", controllers.GetMisNotifs)
- protected.Put("/mis-notifs/:id/leida", controllers.MarcarNotifLeida)
- protected.Post("/mis-notifs/marcar-todas", controllers.MarcarTodasLeidas)
-
protected.Get("/portal-usuarios", middlewares.MenuMiddleware, controllers.PortalUsuariosIndex)
protected.Get("/loadportalusuarios", controllers.LoadPortalUsuarios)
protected.Post("/portal-usuarios", controllers.CreatePortalUsuario)
@@ -290,7 +254,6 @@ func UserRoutes(app fiber.Router) {
protected.Delete("/portal-usuarios/:id", controllers.DeletePortalUsuario)
protected.Post("/portal-usuarios/:id/acceso", controllers.AddPortalAcceso)
protected.Delete("/portal-usuarios/:id/acceso/:clienteID", controllers.RemovePortalAcceso)
- protected.Post("/portal-usuarios/:id/send-credentials", controllers.SendPortalCredentials)
protected.Get("/facturas", middlewares.MenuMiddleware, controllers.FacturasIndex)
protected.Get("/loadfacturas", controllers.LoadFacturas)
|