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/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 { return c.Redirect("/portal/dashboard") } return c.Render("portal/login", fiber.Map{ "error": c.Query("error"), }, "layouts/portal_public") } func PortalLoginPost(c *fiber.Ctx) error { email := strings.TrimSpace(c.FormValue("email")) 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") } func PortalLogout(c *fiber.Ctx) error { _ = auth.DestroyPortalSession(c) return c.Redirect("/portal/login") } // ─── Dashboard ──────────────────────────────────────────────────────────────── 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") } clienteIDs := models.GetClienteIDsForPortalUser(fullUser) proyectos, _ := models.GetProyectosByClienteIDs(clienteIDs) // Si partner: agrupar proyectos por cliente type ClienteProyectos struct { Cliente models.Cliente Proyectos []models.Proyecto } var grupos []ClienteProyectos clienteMap := map[uint]*ClienteProyectos{} for _, p := range proyectos { if _, ok := clienteMap[p.ClienteID]; !ok { clienteMap[p.ClienteID] = &ClienteProyectos{Cliente: p.Cliente} } clienteMap[p.ClienteID].Proyectos = append(clienteMap[p.ClienteID].Proyectos, p) } for _, g := range clienteMap { grupos = append(grupos, *g) } return c.Render("portal/dashboard", fiber.Map{ "portalUser": fullUser, "proyectos": proyectos, "grupos": grupos, "isPartner": fullUser.Rol == "partner" || (fullUser.Role != nil && fullUser.Role.EsPortalPartner), }, "layouts/portal") } // ─── Detalle de proyecto ────────────────────────────────────────────────────── func PortalProyecto(c *fiber.Ctx) error { u := middlewares.PortalUserFromLocals(c) if u == nil { return c.Redirect("/portal/login") } slug := c.Params("slug") proy, err := models.GetProyectoBySlug(slug) if err != nil { return c.Status(404).Render("portal/404", fiber.Map{}, "layouts/portal") } // Verificar acceso fullUser, _ := models.GetPortalUserByID(u.ID) clienteIDs := models.GetClienteIDsForPortalUser(fullUser) hasAccess := false for _, cid := range clienteIDs { if cid == proy.ClienteID { hasAccess = true break } } if !hasAccess { return c.Status(403).Redirect("/portal/dashboard") } fases, _ := models.GetFasesByProyecto(proy.ID) avances, _ := models.GetAvancesByProyecto(proy.ID, true) entregables, _ := models.GetEntregablesByProyecto(proy.ID, true) tickets, _ := models.GetTicketsByPortalUser(u.ID) return c.Render("portal/proyecto", fiber.Map{ "portalUser": fullUser, "proyecto": proy, "fases": fases, "avances": avances, "entregables": entregables, "tickets": tickets, }, "layouts/portal") } // ─── API del portal ─────────────────────────────────────────────────────────── // PortalGetProyectos devuelve los proyectos del usuario logueado (JSON para Alpine). func PortalGetProyectos(c *fiber.Ctx) error { u := middlewares.PortalUserFromLocals(c) if u == nil { return c.Status(401).JSON(fiber.Map{"error": "no autenticado"}) } fullUser, _ := models.GetPortalUserByID(u.ID) clienteIDs := models.GetClienteIDsForPortalUser(fullUser) proyectos, _ := models.GetProyectosByClienteIDs(clienteIDs) return c.JSON(proyectos) } // PortalGetProyectoData devuelve todo el contenido de un proyecto (JSON). func PortalGetProyectoData(c *fiber.Ctx) error { u := middlewares.PortalUserFromLocals(c) if u == nil { return c.Status(401).JSON(fiber.Map{"error": "no autenticado"}) } slug := c.Params("slug") proy, err := models.GetProyectoBySlug(slug) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "no encontrado"}) } // Verificar acceso fullUser, _ := models.GetPortalUserByID(u.ID) clienteIDs := models.GetClienteIDsForPortalUser(fullUser) hasAccess := false for _, cid := range clienteIDs { if cid == proy.ClienteID { hasAccess = true break } } if !hasAccess { return c.Status(403).JSON(fiber.Map{"error": "sin acceso"}) } fases, _ := models.GetFasesByProyecto(proy.ID) avances, _ := models.GetAvancesByProyecto(proy.ID, true) entregables, _ := models.GetEntregablesByProyecto(proy.ID, true) tickets, _ := models.GetTicketsByPortalUser(u.ID) facturas, _ := models.GetFacturasByCliente(proy.ClienteID, true) return c.JSON(fiber.Map{ "proyecto": proy, "fases": fases, "avances": avances, "entregables": entregables, "tickets": tickets, "facturas": facturas, }) } // PortalCrearTicket crea un nuevo ticket de soporte. func PortalCrearTicket(c *fiber.Ctx) error { u := middlewares.PortalUserFromLocals(c) if u == nil { return c.Status(401).JSON(fiber.Map{"error": "no autenticado"}) } type Req struct { ProyectoSlug string `json:"proyecto_slug"` Titulo string `json:"titulo"` Descripcion string `json:"descripcion"` Prioridad string `json:"prioridad"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } if strings.TrimSpace(req.Titulo) == "" { return c.Status(400).JSON(fiber.Map{"error": "El título es requerido"}) } proy, err := models.GetProyectoBySlug(req.ProyectoSlug) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "Proyecto no encontrado"}) } t := &models.ProyectoTicket{ ProyectoID: proy.ID, PortalUserID: u.ID, AutorNombre: u.Nombre, Titulo: req.Titulo, Descripcion: req.Descripcion, Prioridad: req.Prioridad, Estado: "abierto", } if t.Prioridad == "" { t.Prioridad = "media" } if err := models.CreateProyectoTicket(t); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.Status(201).JSON(t) } // PortalResponderTicket agrega un mensaje a un ticket. func PortalResponderTicket(c *fiber.Ctx) error { u := middlewares.PortalUserFromLocals(c) if u == nil { return c.Status(401).JSON(fiber.Map{"error": "no autenticado"}) } ticketID, _ := c.ParamsInt("id") 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()}) } if strings.TrimSpace(req.Contenido) == "" { return c.Status(400).JSON(fiber.Map{"error": "El mensaje no puede estar vacío"}) } ticket, err := models.GetTicketByID(uint(ticketID)) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "Ticket no encontrado"}) } if ticket.PortalUserID != u.ID { return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"}) } msg := &models.TicketMensaje{ TicketID: uint(ticketID), Contenido: req.Contenido, EsAdmin: false, AutorNombre: u.Nombre, } if err := models.CreateTicketMensaje(msg); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.Status(201).JSON(msg) } // PortalDownloadEntregable descarga un entregable del portal. func PortalDownloadEntregable(c *fiber.Ctx) error { u := middlewares.PortalUserFromLocals(c) if u == nil { return c.Status(401).JSON(fiber.Map{"error": "no autenticado"}) } entID, _ := c.ParamsInt("id") item, err := models.GetProyectoEntregableByID(uint(entID)) if err != nil || !item.Visible { return c.Status(404).JSON(fiber.Map{"error": "No encontrado"}) } // Verificar que el proyecto pertenece al cliente del portal user proy, err := models.GetProyectoByID(item.ProyectoID) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "Proyecto no encontrado"}) } fullUser, _ := models.GetPortalUserByID(u.ID) clienteIDs := models.GetClienteIDsForPortalUser(fullUser) hasAccess := false for _, cid := range clienteIDs { if cid == proy.ClienteID { hasAccess = true break } } if !hasAccess { return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"}) } clean := item.Archivo 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)) return c.SendFile(clean) } // PortalDownloadFactura descarga el PDF de una factura del portal. func PortalDownloadFactura(c *fiber.Ctx) error { u := middlewares.PortalUserFromLocals(c) if u == nil { return c.Status(401).JSON(fiber.Map{"error": "no autenticado"}) } factID, _ := c.ParamsInt("id") f, err := models.GetFacturaByID(uint(factID)) if err != nil || !f.Visible || f.Archivo == "" { return c.Status(404).JSON(fiber.Map{"error": "No encontrado"}) } // Verificar acceso fullUser, _ := models.GetPortalUserByID(u.ID) clienteIDs := models.GetClienteIDsForPortalUser(fullUser) hasAccess := false for _, cid := range clienteIDs { if cid == f.ClienteID { hasAccess = true break } } if !hasAccess { return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"}) } clean := f.Archivo if !strings.HasPrefix(clean, "uploads/") { return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"}) } c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, f.OriginalName)) return c.SendFile(clean) }