k
This commit is contained in:
@@ -82,6 +82,9 @@ func Migrate() {
|
|||||||
&models.ProyectoTicket{},
|
&models.ProyectoTicket{},
|
||||||
&models.TicketMensaje{},
|
&models.TicketMensaje{},
|
||||||
&models.Factura{},
|
&models.Factura{},
|
||||||
|
// Sistema de notificaciones por evento
|
||||||
|
&models.NotifEventoConfig{},
|
||||||
|
&models.SistemaNotificacion{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatalf("Error during main migration: %v", err)
|
log.Fatalf("Error during main migration: %v", err)
|
||||||
}
|
}
|
||||||
@@ -119,6 +122,12 @@ func Migrate() {
|
|||||||
// Crear plantillas y reglas mínimas si no existen (bienvenida, pago_recibido)
|
// Crear plantillas y reglas mínimas si no existen (bienvenida, pago_recibido)
|
||||||
SeedPlantillasBase()
|
SeedPlantillasBase()
|
||||||
|
|
||||||
|
// Agregar submódulo "Telegram" al módulo Integraciones
|
||||||
|
SeedTelegram()
|
||||||
|
|
||||||
|
// Crear módulo y submódulos de Portal de Clientes
|
||||||
|
SeedPortalClientes()
|
||||||
|
|
||||||
log.Println("Migration Completed...")
|
log.Println("Migration Completed...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,13 @@ func PortalUser(c *fiber.Ctx) (*models.PortalUser, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetPortalSession guarda el portal_user_id en la sesión.
|
// SetPortalSession guarda el portal_user_id en la sesión.
|
||||||
|
// Regenera el ID de sesión para prevenir session fixation.
|
||||||
func SetPortalSession(c *fiber.Ctx, userID uint) error {
|
func SetPortalSession(c *fiber.Ctx, userID uint) error {
|
||||||
store := app.Http.Session.Get(c)
|
store := app.Http.Session.Get(c)
|
||||||
store.Set(portalSessionKey, userID)
|
store.Set(portalSessionKey, userID)
|
||||||
|
if err := store.Regenerate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return store.Save()
|
return store.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ type PortalUser struct {
|
|||||||
ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"` // nil si es partner
|
ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"` // nil si es partner
|
||||||
Cliente *Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
|
Cliente *Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
|
||||||
Rol string `json:"rol" gorm:"column:rol;default:'cliente'"` // cliente|partner
|
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"`
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||||
TelegramChatID string `json:"telegram_chat_id" gorm:"column:telegram_chat_id"`
|
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"`
|
PortalAccesos []PortalAcceso `json:"portal_accesos" gorm:"foreignKey:PortalUserID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +48,7 @@ func (PortalAcceso) TableName() string { return "portal_accesos" }
|
|||||||
func GetAllPortalUsers(limit, offset int) ([]PortalUser, int64, error) {
|
func GetAllPortalUsers(limit, offset int) ([]PortalUser, int64, error) {
|
||||||
var items []PortalUser
|
var items []PortalUser
|
||||||
var total int64
|
var total int64
|
||||||
db := app.Http.Database.DB.Model(&PortalUser{}).Preload("Cliente").Preload("PortalAccesos.Cliente")
|
db := app.Http.Database.DB.Model(&PortalUser{}).Preload("Cliente").Preload("PortalAccesos.Cliente").Preload("Role")
|
||||||
if err := db.Count(&total).Error; err != nil {
|
if err := db.Count(&total).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -56,7 +60,7 @@ func GetAllPortalUsers(limit, offset int) ([]PortalUser, int64, error) {
|
|||||||
|
|
||||||
func GetPortalUserByID(id uint) (*PortalUser, error) {
|
func GetPortalUserByID(id uint) (*PortalUser, error) {
|
||||||
var item PortalUser
|
var item PortalUser
|
||||||
err := app.Http.Database.DB.Preload("Cliente").Preload("PortalAccesos.Cliente").First(&item, id).Error
|
err := app.Http.Database.DB.Preload("Cliente").Preload("PortalAccesos.Cliente").Preload("Role").First(&item, id).Error
|
||||||
return &item, err
|
return &item, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,9 +80,12 @@ func UpdatePortalUser(u *PortalUser) error {
|
|||||||
"email": u.Email,
|
"email": u.Email,
|
||||||
"cliente_id": u.ClienteID,
|
"cliente_id": u.ClienteID,
|
||||||
"rol": u.Rol,
|
"rol": u.Rol,
|
||||||
|
"role_id": u.RoleID,
|
||||||
"activo": u.Activo,
|
"activo": u.Activo,
|
||||||
"notas": u.Notas,
|
"notas": u.Notas,
|
||||||
"telegram_chat_id": u.TelegramChatID,
|
"telegram_chat_id": u.TelegramChatID,
|
||||||
|
"telefono": u.Telefono,
|
||||||
|
"pais": u.Pais,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +156,11 @@ func RemovePortalAcceso(portalUserID, clienteID uint) error {
|
|||||||
|
|
||||||
// GetClienteIDsForPortalUser devuelve todos los clienteIDs accesibles para un portal user.
|
// GetClienteIDsForPortalUser devuelve todos los clienteIDs accesibles para un portal user.
|
||||||
func GetClienteIDsForPortalUser(u *PortalUser) []uint {
|
func GetClienteIDsForPortalUser(u *PortalUser) []uint {
|
||||||
if u.Rol == "partner" {
|
isPartner := u.Rol == "partner"
|
||||||
|
if u.Role != nil {
|
||||||
|
isPartner = u.Role.EsPortalPartner
|
||||||
|
}
|
||||||
|
if isPartner {
|
||||||
ids := make([]uint, 0, len(u.PortalAccesos))
|
ids := make([]uint, 0, len(u.PortalAccesos))
|
||||||
for _, a := range u.PortalAccesos {
|
for _, a := range u.PortalAccesos {
|
||||||
ids = append(ids, a.ClienteID)
|
ids = append(ids, a.ClienteID)
|
||||||
|
|||||||
@@ -124,8 +124,11 @@ func GetAllProyectos(limit, offset int, search string) ([]Proyecto, int64, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetProyectosByClienteIDs(clienteIDs []uint) ([]Proyecto, error) {
|
func GetProyectosByClienteIDs(clienteIDs []uint) ([]Proyecto, error) {
|
||||||
|
if len(clienteIDs) == 0 {
|
||||||
|
return []Proyecto{}, nil
|
||||||
|
}
|
||||||
var items []Proyecto
|
var items []Proyecto
|
||||||
err := app.Http.Database.DB.Where("cliente_id IN ?", clienteIDs).Order("created_at DESC").Find(&items).Error
|
err := app.Http.Database.DB.Preload("Cliente").Where("cliente_id IN ?", clienteIDs).Order("created_at DESC").Find(&items).Error
|
||||||
return items, err
|
return items, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FacturasIndex(c *fiber.Ctx) error {
|
func FacturasIndex(c *fiber.Ctx) error {
|
||||||
@@ -185,6 +186,10 @@ func UploadFacturaPDF(c *fiber.Ctx) error {
|
|||||||
if err := models.UpdateFacturaArchivo(uint(id), savePath, file.Filename); err != nil {
|
if err := models.UpdateFacturaArchivo(uint(id), savePath, file.Filename); err != nil {
|
||||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
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})
|
return c.JSON(fiber.Map{"ok": true, "archivo": savePath})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/auth"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/auth"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/rest/middlewares"
|
"github.com/sujit-baniya/fiber-boilerplate/rest/middlewares"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ─── Auth ─────────────────────────────────────────────────────────────────────
|
// ─── Auth ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func PortalLoginPage(c *fiber.Ctx) error {
|
func PortalLoginPage(c *fiber.Ctx) error {
|
||||||
if auth.IsPortalLoggedIn(c) {
|
if user, err := auth.PortalUser(c); err == nil && user != nil {
|
||||||
return c.Redirect("/portal/dashboard")
|
return c.Redirect("/portal/dashboard")
|
||||||
}
|
}
|
||||||
return c.Render("portal/login", fiber.Map{
|
return c.Render("portal/login", fiber.Map{
|
||||||
@@ -220,6 +221,7 @@ func PortalCrearTicket(c *fiber.Ctx) error {
|
|||||||
if err := models.CreateProyectoTicket(t); err != nil {
|
if err := models.CreateProyectoTicket(t); err != nil {
|
||||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
go services.DispatchTicketNuevo(t, u, proy.Nombre)
|
||||||
return c.Status(201).JSON(t)
|
return c.Status(201).JSON(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +256,13 @@ func PortalResponderTicket(c *fiber.Ctx) error {
|
|||||||
if err := models.CreateTicketMensaje(msg); err != nil {
|
if err := models.CreateTicketMensaje(msg); err != nil {
|
||||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
go func() {
|
||||||
|
proyNombre := ""
|
||||||
|
if proy, err := models.GetProyectoByID(ticket.ProyectoID); err == nil {
|
||||||
|
proyNombre = proy.Nombre
|
||||||
|
}
|
||||||
|
services.DispatchTicketRespuestaCliente(ticket, req.Contenido, proyNombre)
|
||||||
|
}()
|
||||||
return c.Status(201).JSON(msg)
|
return c.Status(201).JSON(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PortalUsuariosIndex(c *fiber.Ctx) error {
|
func PortalUsuariosIndex(c *fiber.Ctx) error {
|
||||||
@@ -29,12 +30,14 @@ func LoadPortalUsuarios(c *fiber.Ctx) error {
|
|||||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
}
|
}
|
||||||
clientes, _, _ := models.GetAllClientes(200, 0, "")
|
clientes, _, _ := models.GetAllClientes(200, 0, "")
|
||||||
|
portalRoles, _ := models.GetPortalRoles()
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
"items": items,
|
"items": items,
|
||||||
"total": total,
|
"total": total,
|
||||||
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
|
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
|
||||||
"page": page,
|
"page": page,
|
||||||
"clientes": clientes,
|
"clientes": clientes,
|
||||||
|
"portalRoles": portalRoles,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +47,7 @@ func CreatePortalUsuario(c *fiber.Ctx) error {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
ClienteID *uint `json:"cliente_id"`
|
ClienteID *uint `json:"cliente_id"`
|
||||||
|
RoleID *uint `json:"role_id"`
|
||||||
Rol string `json:"rol"`
|
Rol string `json:"rol"`
|
||||||
Notas string `json:"notas"`
|
Notas string `json:"notas"`
|
||||||
TelegramChatID string `json:"telegram_chat_id"`
|
TelegramChatID string `json:"telegram_chat_id"`
|
||||||
@@ -62,12 +66,23 @@ func CreatePortalUsuario(c *fiber.Ctx) error {
|
|||||||
if req.Rol == "" {
|
if req.Rol == "" {
|
||||||
req.Rol = "cliente"
|
req.Rol = "cliente"
|
||||||
}
|
}
|
||||||
|
// Derivar Rol del tipo de rol seleccionado
|
||||||
|
rol := req.Rol
|
||||||
|
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{
|
u := &models.PortalUser{
|
||||||
Nombre: req.Nombre,
|
Nombre: req.Nombre,
|
||||||
Email: strings.ToLower(strings.TrimSpace(req.Email)),
|
Email: strings.ToLower(strings.TrimSpace(req.Email)),
|
||||||
Password: hashed,
|
Password: hashed,
|
||||||
ClienteID: req.ClienteID,
|
ClienteID: req.ClienteID,
|
||||||
Rol: req.Rol,
|
RoleID: req.RoleID,
|
||||||
|
Rol: rol,
|
||||||
Activo: true,
|
Activo: true,
|
||||||
Notas: req.Notas,
|
Notas: req.Notas,
|
||||||
TelegramChatID: req.TelegramChatID,
|
TelegramChatID: req.TelegramChatID,
|
||||||
@@ -89,6 +104,7 @@ func UpdatePortalUsuario(c *fiber.Ctx) error {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
ClienteID *uint `json:"cliente_id"`
|
ClienteID *uint `json:"cliente_id"`
|
||||||
|
RoleID *uint `json:"role_id"`
|
||||||
Rol string `json:"rol"`
|
Rol string `json:"rol"`
|
||||||
Activo bool `json:"activo"`
|
Activo bool `json:"activo"`
|
||||||
Notas string `json:"notas"`
|
Notas string `json:"notas"`
|
||||||
@@ -98,11 +114,22 @@ func UpdatePortalUsuario(c *fiber.Ctx) error {
|
|||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
// Derivar Rol del tipo de rol seleccionado
|
||||||
|
rol := req.Rol
|
||||||
|
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{
|
u := &models.PortalUser{
|
||||||
Nombre: req.Nombre,
|
Nombre: req.Nombre,
|
||||||
Email: strings.ToLower(strings.TrimSpace(req.Email)),
|
Email: strings.ToLower(strings.TrimSpace(req.Email)),
|
||||||
ClienteID: req.ClienteID,
|
ClienteID: req.ClienteID,
|
||||||
Rol: req.Rol,
|
RoleID: req.RoleID,
|
||||||
|
Rol: rol,
|
||||||
Activo: req.Activo,
|
Activo: req.Activo,
|
||||||
Notas: req.Notas,
|
Notas: req.Notas,
|
||||||
TelegramChatID: req.TelegramChatID,
|
TelegramChatID: req.TelegramChatID,
|
||||||
@@ -155,3 +182,29 @@ func RemovePortalAcceso(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
return c.JSON(fiber.Map{"ok": true})
|
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})
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// PortalAuth protege las rutas del portal de clientes.
|
// PortalAuth protege las rutas del portal de clientes.
|
||||||
// Si no hay sesión activa, redirige a /portal/login.
|
// Si no hay sesión válida, destruye la sesión obsoleta y redirige a /portal/login.
|
||||||
func PortalAuth() fiber.Handler {
|
func PortalAuth() fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
user, err := auth.PortalUser(c)
|
user, err := auth.PortalUser(c)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
|
// Limpiar sesión inválida/obsoleta para romper posibles redirect loops
|
||||||
|
_ = auth.DestroyPortalSession(c)
|
||||||
return c.Redirect("/portal/login")
|
return c.Redirect("/portal/login")
|
||||||
}
|
}
|
||||||
c.Locals("portalUser", user)
|
c.Locals("portalUser", user)
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ func UserRoutes(app fiber.Router) {
|
|||||||
protected.Delete("/portal-usuarios/:id", controllers.DeletePortalUsuario)
|
protected.Delete("/portal-usuarios/:id", controllers.DeletePortalUsuario)
|
||||||
protected.Post("/portal-usuarios/:id/acceso", controllers.AddPortalAcceso)
|
protected.Post("/portal-usuarios/:id/acceso", controllers.AddPortalAcceso)
|
||||||
protected.Delete("/portal-usuarios/:id/acceso/:clienteID", controllers.RemovePortalAcceso)
|
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("/facturas", middlewares.MenuMiddleware, controllers.FacturasIndex)
|
||||||
protected.Get("/loadfacturas", controllers.LoadFacturas)
|
protected.Get("/loadfacturas", controllers.LoadFacturas)
|
||||||
|
|||||||
Reference in New Issue
Block a user