diff --git a/README.md b/README.md index 46ea7e2..c0f66d8 100755 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # SISTEMA DE WEBSITE -Este sistema ha sido desarrollado por U-Site para Gases del Oriente con el propósito de optimizar la gestión administrativa, mejorar la experiencia del usuario y facilitar el acceso a información clave. Está diseñado para centralizar la administración de contenidos web, trámites virtuales y configuraciones del sistema, asegurando eficiencia, seguridad y personalización según las necesidades de la organización y sus usuarios. +Este sistema ha sido desarrollado por U-Site para Usite SAS BIC con el propósito de optimizar la gestión administrativa, mejorar la experiencia del usuario y facilitar el acceso a información clave. Está diseñado para centralizar la administración de contenidos web, trámites virtuales y configuraciones del sistema, asegurando eficiencia, seguridad y personalización según las necesidades de la organización y sus usuarios. ## Descripción -El software permite crear una plataforma moderna y segura que optimiza la experiencia del usuario y asegura la protección de datos. Ofrece una página web con un diseño atractivo y funcional, adaptado a diversos dispositivos y navegadores, mientras que la oficina virtual proporciona herramientas colaborativas y de comunicación para la gestión interna. Entre sus principales funcionalidades, incluye una interfaz intuitiva, medidas de seguridad avanzadas, y una experiencia optimizada para dispositivos móviles y de escritorio. Además, ofrece soporte continuo y mantenimiento para asegurar el buen funcionamiento de la plataforma, mejorando la seguridad cibernética y reforzando la presencia digital de GASES DEL ORIENTE S.A E.S.P. +El software permite crear una plataforma moderna y segura que optimiza la experiencia del usuario y asegura la protección de datos. Ofrece una página web con un diseño atractivo y funcional, adaptado a diversos dispositivos y navegadores, mientras que la oficina virtual proporciona herramientas colaborativas y de comunicación para la gestión interna. Entre sus principales funcionalidades, incluye una interfaz intuitiva, medidas de seguridad avanzadas, y una experiencia optimizada para dispositivos móviles y de escritorio. Además, ofrece soporte continuo y mantenimiento para asegurar el buen funcionamiento de la plataforma, mejorando la seguridad cibernética y reforzando la presencia digital de Usite SAS BIC. ## Requisitos del Sistema @@ -13,8 +13,8 @@ El software permite crear una plataforma moderna y segura que optimiza la experi ## Instalación -1. Clona el repositorio de GitHub: `git clone https://github.com/lizandrogd/gases.git` -2. Accede al directorio del proyecto: `cd gases` +1. Clona el repositorio de GitHub: `git clone https://github.com/lizandrogd/soft_usite.git` +2. Accede al directorio del proyecto: `cd soft_usite` 3. Instala las dependencias de go: `go mod tidy` ## Uso diff --git a/config.sample.yml b/config.sample.yml index 6e13018..94987c8 100755 --- a/config.sample.yml +++ b/config.sample.yml @@ -43,7 +43,7 @@ mail: password: encryption: tls from_address: "itsursujit@gmail.com" - from_name: "Sujit Baniya" + from_name: "Usite SAS BIC" token: app_jwt_secret: SECRET_APP diff --git a/config/mail.go b/config/mail.go index 3671b5c..989176d 100755 --- a/config/mail.go +++ b/config/mail.go @@ -16,11 +16,11 @@ type Mail struct { *mail.SMTPServer *mail.SMTPClient Host string `mapstructure:"MAIL_HOST" yaml:"host" env:"MAIL_HOST" env-default:"smtp-mail.outlook.com"` - Username string `mapstructure:"MAIL_USERNAME" yaml:"username" env:"MAIL_USERNAME" env-default:"ovirtual@gasesdeloriente.com.co"` - Password string `mapstructure:"MAIL_PASSWORD" yaml:"password" env:"MAIL_PASSWORD" env-default:"Gases2023**"` + Username string `mapstructure:"MAIL_USERNAME" yaml:"username" env:"MAIL_USERNAME" env-default:"info@u-site.app"` + Password string `mapstructure:"MAIL_PASSWORD" yaml:"password" env:"MAIL_PASSWORD" env-default:""` Encryption string `mapstructure:"MAIL_ENCRYPTION" yaml:"encryption" env:"MAIL_ENCRYPTION" env-default:"tls"` - FromAddress string `mapstructure:"MAIL_FROM_ADDRESS" yaml:"from_address" env:"MAIL_FROM_ADDRESS" env-default:"ovirtual@gasesdeloriente.com.co"` - FromName string `mapstructure:"MAIL_FROM_NAME" yaml:"from_name" env:"MAIL_FROM_NAME" env-default:"Gases"` + FromAddress string `mapstructure:"MAIL_FROM_ADDRESS" yaml:"from_address" env:"MAIL_FROM_ADDRESS" env-default:"info@u-site.app"` + FromName string `mapstructure:"MAIL_FROM_NAME" yaml:"from_name" env:"MAIL_FROM_NAME" env-default:"Usite SAS BIC"` View *ViewConfig Port int `mapstructure:"MAIL_PORT" yaml:"port" env:"MAIL_PORT" env-default:"587"` // Cambié a 465 para SSL } diff --git a/pkg/services/app_url.go b/pkg/services/app_url.go new file mode 100644 index 0000000..8f64d51 --- /dev/null +++ b/pkg/services/app_url.go @@ -0,0 +1,67 @@ +package services + +import ( + "fmt" + "html" + "net" + "net/url" + "strings" + + "github.com/sujit-baniya/fiber-boilerplate/app" +) + +// getAppURL devuelve la URL base absoluta (sin slash final). +// Si APP_URL es http://localhost sin puerto, añade APP_PORT para enlaces en desarrollo. +func getAppURL() string { + u := strings.TrimRight(app.Http.Server.Url, "/") + if u == "" { + return fmt.Sprintf("http://localhost:%s", app.Http.Server.Port) + } + + parsed, err := url.Parse(u) + if err != nil || parsed.Host == "" { + return u + } + + host := parsed.Hostname() + if parsed.Port() == "" && app.Http.Server.Port != "" { + if host == "localhost" || host == "127.0.0.1" { + parsed.Host = net.JoinHostPort(host, app.Http.Server.Port) + return strings.TrimRight(parsed.String(), "/") + } + } + return u +} + +// absAppURL convierte una ruta relativa en URL absoluta para correo/Telegram. +func absAppURL(path string) string { + if path == "" { + return getAppURL() + } + if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { + return path + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + return getAppURL() + path +} + +func adminTicketPath(ticketID uint) string { + return fmt.Sprintf("/app/tickets?ticket=%d", ticketID) +} + +func portalTicketPath(proyectoSlug string, ticketID uint) string { + if proyectoSlug == "" { + return "/portal/dashboard" + } + return fmt.Sprintf("/portal/proyecto/%s?tab=Tickets&ticket=%d", url.PathEscape(proyectoSlug), ticketID) +} + +func escapeTelegramHTML(s string) string { + return html.EscapeString(s) +} + +func telegramHTMLLink(absURL, label string) string { + return fmt.Sprintf(`%s`, escapeTelegramHTML(absURL), escapeTelegramHTML(label)) +} diff --git a/pkg/services/app_url_test.go b/pkg/services/app_url_test.go new file mode 100644 index 0000000..acbadbe --- /dev/null +++ b/pkg/services/app_url_test.go @@ -0,0 +1,41 @@ +package services + +import ( + "testing" + + "github.com/sujit-baniya/fiber-boilerplate/app" + "github.com/sujit-baniya/fiber-boilerplate/config" +) + +func TestGetAppURLLocalhostAddsPort(t *testing.T) { + app.Http = &config.AppConfig{ + Server: config.ServerConfig{ + Url: "http://localhost", + Port: "8084", + }, + } + if got := getAppURL(); got != "http://localhost:8084" { + t.Fatalf("getAppURL() = %q, want http://localhost:8084", got) + } +} + +func TestAbsAppURLRelativePath(t *testing.T) { + app.Http = &config.AppConfig{ + Server: config.ServerConfig{ + Url: "http://localhost", + Port: "8084", + }, + } + got := absAppURL("/app/tickets?ticket=3") + want := "http://localhost:8084/app/tickets?ticket=3" + if got != want { + t.Fatalf("absAppURL() = %q, want %q", got, want) + } +} + +func TestTelegramHTMLLinkEscapesAmpersand(t *testing.T) { + link := telegramHTMLLink("http://localhost:8084/portal/p?tab=Tickets&ticket=1", "Ver portal") + if link != `Ver portal` { + t.Fatalf("telegramHTMLLink() = %q", link) + } +} diff --git a/pkg/services/notif_dispatch.go b/pkg/services/notif_dispatch.go index 11365f2..7cf5184 100644 --- a/pkg/services/notif_dispatch.go +++ b/pkg/services/notif_dispatch.go @@ -3,8 +3,8 @@ package services import ( "fmt" "log" + "net/url" - "github.com/sujit-baniya/fiber-boilerplate/app" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) @@ -18,8 +18,8 @@ func DispatchTicketNuevo(ticket *models.ProyectoTicket, portalUser *models.Porta if cfg == nil { return } - baseURL := getAppURL() - ticketURL := baseURL + "/app/tickets" + ticketPath := adminTicketPath(ticket.ID) + ticketURL := absAppURL(ticketPath) titulo := fmt.Sprintf("Nuevo ticket: %s", ticket.Titulo) cuerpo := fmt.Sprintf("Cliente: %s\nProyecto: %s\n%s", ticket.AutorNombre, proyectoNombre, ticket.Descripcion) @@ -29,7 +29,7 @@ func DispatchTicketNuevo(ticket *models.ProyectoTicket, portalUser *models.Porta UsuarioID: 0, Titulo: titulo, Cuerpo: cuerpo, - Url: ticketURL, + Url: ticketPath, Icono: "🎫", }) } @@ -39,8 +39,12 @@ func DispatchTicketNuevo(ticket *models.ProyectoTicket, portalUser *models.Porta } } if cfg.CanalTelegram { - msg := fmt.Sprintf("🎫 Nuevo ticket\nProyecto: %s\nCliente: %s\nTítulo: %s\n\n%s\n\n🔗 Ver tickets", - proyectoNombre, ticket.AutorNombre, ticket.Titulo, ticket.Descripcion, ticketURL) + msg := fmt.Sprintf("🎫 Nuevo ticket\nProyecto: %s\nCliente: %s\nTítulo: %s\n\n%s\n\n🔗 %s", + escapeTelegramHTML(proyectoNombre), + escapeTelegramHTML(ticket.AutorNombre), + escapeTelegramHTML(ticket.Titulo), + escapeTelegramHTML(ticket.Descripcion), + telegramHTMLLink(ticketURL, "Ver ticket")) sendTelegramAdmin(msg) } } @@ -55,8 +59,8 @@ func DispatchTicketRespuestaCliente(ticket *models.ProyectoTicket, contenido str if cfg == nil { return } - baseURL := getAppURL() - ticketURL := baseURL + "/app/tickets" + ticketPath := adminTicketPath(ticket.ID) + ticketURL := absAppURL(ticketPath) titulo := fmt.Sprintf("Respuesta de cliente en: %s", ticket.Titulo) cuerpo := fmt.Sprintf("Cliente: %s\nProyecto: %s\n%s", ticket.AutorNombre, proyectoNombre, contenido) @@ -66,7 +70,7 @@ func DispatchTicketRespuestaCliente(ticket *models.ProyectoTicket, contenido str UsuarioID: 0, Titulo: titulo, Cuerpo: cuerpo, - Url: ticketURL, + Url: ticketPath, Icono: "💬", }) } @@ -76,8 +80,11 @@ func DispatchTicketRespuestaCliente(ticket *models.ProyectoTicket, contenido str } } if cfg.CanalTelegram { - msg := fmt.Sprintf("💬 Respuesta de cliente\nProyecto: %s\nCliente: %s\n\n%s\n\n🔗 Ver tickets", - proyectoNombre, ticket.AutorNombre, contenido, ticketURL) + msg := fmt.Sprintf("💬 Respuesta de cliente\nProyecto: %s\nCliente: %s\n\n%s\n\n🔗 %s", + escapeTelegramHTML(proyectoNombre), + escapeTelegramHTML(ticket.AutorNombre), + escapeTelegramHTML(contenido), + telegramHTMLLink(ticketURL, "Ver ticket")) sendTelegramAdmin(msg) } } @@ -99,8 +106,12 @@ func DispatchTicketRespuestaAdmin(ticket *models.ProyectoTicket, contenido strin return } - baseURL := getAppURL() - portalURL := baseURL + "/portal/dashboard" + proyectoSlug := "" + if proy, err := models.GetProyectoByID(ticket.ProyectoID); err == nil { + proyectoSlug = proy.Slug + } + portalPath := portalTicketPath(proyectoSlug, ticket.ID) + portalURL := absAppURL(portalPath) titulo := fmt.Sprintf("Respuesta en tu ticket: %s", ticket.Titulo) cuerpo := fmt.Sprintf("El equipo respondió: %s", contenido) @@ -110,7 +121,7 @@ func DispatchTicketRespuestaAdmin(ticket *models.ProyectoTicket, contenido strin UsuarioID: portalUser.ID, Titulo: titulo, Cuerpo: cuerpo, - Url: portalURL, + Url: portalPath, Icono: "💬", }) } @@ -119,8 +130,11 @@ func DispatchTicketRespuestaAdmin(ticket *models.ProyectoTicket, contenido strin } if cfg.CanalTelegram && portalUser.TelegramChatID != "" { ts := NewTelegramService() - msg := fmt.Sprintf("💬 El equipo respondió tu ticket\nProyecto: %s\nTicket: %s\n\n%s\n\n🔗 Ver tu portal", - proyectoNombre, ticket.Titulo, contenido, portalURL) + msg := fmt.Sprintf("💬 El equipo respondió tu ticket\nProyecto: %s\nTicket: %s\n\n%s\n\n🔗 %s", + escapeTelegramHTML(proyectoNombre), + escapeTelegramHTML(ticket.Titulo), + escapeTelegramHTML(contenido), + telegramHTMLLink(portalURL, "Ver en el portal")) if err := ts.SendMessageWithToken(portalUser.TelegramChatID, msg, getAdminBotToken()); err != nil { log.Printf("[Notif] Error telegram portal_user %d: %v", portalUser.ID, err) } @@ -147,12 +161,11 @@ func DispatchFacturaSubida(factura *models.Factura) { clienteNombre = factura.Cliente.Nombre } - baseURL := getAppURL() - // Construir URL apuntando a la pestaña Facturas del proyecto (o dashboard si no tiene proyecto) - portalURL := baseURL + "/portal/dashboard" + portalPath := "/portal/dashboard" if factura.ProyectoID != nil && factura.Proyecto != nil && factura.Proyecto.Slug != "" { - portalURL = baseURL + "/portal/proyecto/" + factura.Proyecto.Slug + "?tab=Facturas" + portalPath = fmt.Sprintf("/portal/proyecto/%s?tab=Facturas", url.PathEscape(factura.Proyecto.Slug)) } + portalURL := absAppURL(portalPath) for _, u := range portalUsers { u := u @@ -165,7 +178,7 @@ func DispatchFacturaSubida(factura *models.Factura) { UsuarioID: u.ID, Titulo: titulo, Cuerpo: cuerpo, - Url: portalURL, + Url: portalPath, Icono: "🧾", }) } @@ -174,8 +187,11 @@ func DispatchFacturaSubida(factura *models.Factura) { } if cfg.CanalTelegram && u.TelegramChatID != "" { ts := NewTelegramService() - msg := fmt.Sprintf("🧾 Nueva factura disponible\nNúmero: %s\nMonto: %s %.2f\n\n🔗 Ver en el portal", - factura.Numero, factura.Moneda, factura.Monto, portalURL) + msg := fmt.Sprintf("🧾 Nueva factura disponible\nNúmero: %s\nMonto: %s %.2f\n\n🔗 %s", + escapeTelegramHTML(factura.Numero), + escapeTelegramHTML(factura.Moneda), + factura.Monto, + telegramHTMLLink(portalURL, "Ver en el portal")) if err := ts.SendMessageWithToken(u.TelegramChatID, msg, getAdminBotToken()); err != nil { log.Printf("[Notif] Error telegram portal_user %d: %v", u.ID, err) } @@ -233,13 +249,3 @@ func sendTelegramAdmin(mensaje string) { _ = models.CreateTelegramLog(logEntry) } } - -// getAppURL devuelve la URL base configurada en APP_URL (sin slash final). -func getAppURL() string { - u := app.Http.Server.Url - // quitar slash final si lo hay - if len(u) > 0 && u[len(u)-1] == '/' { - u = u[:len(u)-1] - } - return u -} diff --git a/resources/views/portal/proyecto.html b/resources/views/portal/proyecto.html index 55897b4..085fa5b 100644 --- a/resources/views/portal/proyecto.html +++ b/resources/views/portal/proyecto.html @@ -166,7 +166,7 @@