From 4fa506c7d620ccaa7d24dcf243dfacd825945c54 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sat, 16 May 2026 21:27:30 -0500 Subject: [PATCH] up --- migrations/migrate.go | 1 + pkg/models/portal_user.go | 17 +- pkg/models/telegram_portal_token.go | 50 +++++ pkg/services/telegram_service.go | 22 +++ resources/views/layouts/portal.html | 172 +++++++++++++++++- resources/views/portal_usuarios.html | 128 +++++++++++++ rest/controllers/portal_controller.go | 164 ++++++++++++++++- rest/controllers/portal_usuario_controller.go | 29 +++ rest/routes/portal.go | 5 + rest/routes/publicas.go | 4 +- rest/routes/user.go | 7 +- 11 files changed, 586 insertions(+), 13 deletions(-) create mode 100644 pkg/models/telegram_portal_token.go diff --git a/migrations/migrate.go b/migrations/migrate.go index c9f2455..6a0bf9a 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -738,6 +738,7 @@ func MigratePortal() { &models.TicketMensaje{}, &models.PortalUser{}, &models.PortalAcceso{}, + &models.TelegramPortalToken{}, &models.Factura{}, ); err != nil { log.Printf("[MIGRATE] Error en MigratePortal: %v", err) diff --git a/pkg/models/portal_user.go b/pkg/models/portal_user.go index b641690..879af06 100644 --- a/pkg/models/portal_user.go +++ b/pkg/models/portal_user.go @@ -28,8 +28,10 @@ type PortalUser struct { Telefono string `json:"telefono" gorm:"column:telefono"` Indicativo string `json:"indicativo" gorm:"column:indicativo"` // ej: +57, +1 Pais string `json:"pais" gorm:"column:pais"` - Documento string `json:"documento" gorm:"column:documento"` // RUT, NIT, CC, etc. - Empresa string `json:"empresa" gorm:"column:empresa"` // empresa que representa + Documento string `json:"documento" gorm:"column:documento"` // RUT, NIT, CC, etc. + Empresa string `json:"empresa" gorm:"column:empresa"` // empresa que representa + DocumentoRutFile string `json:"documento_rut_file" gorm:"column:documento_rut_file"` // ruta del archivo RUT + DocumentoRutNombre string `json:"documento_rut_nombre" gorm:"column:documento_rut_nombre"` // nombre original PortalAccesos []PortalAcceso `json:"portal_accesos" gorm:"foreignKey:PortalUserID"` } @@ -99,6 +101,17 @@ func UpdatePortalUserPassword(id uint, hashedPassword string) error { return app.Http.Database.DB.Model(&PortalUser{}).Where("id = ?", id).Update("password", hashedPassword).Error } +func UpdatePortalUserTelegramChatID(id uint, chatID string) error { + return app.Http.Database.DB.Model(&PortalUser{}).Where("id = ?", id).Update("telegram_chat_id", chatID).Error +} + +func UpdatePortalUserRutFile(id uint, path, nombre string) error { + return app.Http.Database.DB.Model(&PortalUser{}).Where("id = ?", id).Updates(map[string]interface{}{ + "documento_rut_file": path, + "documento_rut_nombre": nombre, + }).Error +} + func DeletePortalUser(id uint) error { // Eliminar también sus accesos de partner if err := app.Http.Database.DB.Where("portal_user_id = ?", id).Delete(&PortalAcceso{}).Error; err != nil { diff --git a/pkg/models/telegram_portal_token.go b/pkg/models/telegram_portal_token.go new file mode 100644 index 0000000..75b12f4 --- /dev/null +++ b/pkg/models/telegram_portal_token.go @@ -0,0 +1,50 @@ +package models + +import ( + "crypto/rand" + "fmt" + + "github.com/sujit-baniya/fiber-boilerplate/app" + "gorm.io/gorm" +) + +// TelegramPortalToken almacena un código de verificación temporal para vincular +// el Telegram de un usuario del portal con el bot de notificaciones. +type TelegramPortalToken struct { + gorm.Model + PortalUserID uint `json:"portal_user_id" gorm:"column:portal_user_id;uniqueIndex"` + Token string `json:"token" gorm:"column:token;uniqueIndex;size:8"` +} + +func (TelegramPortalToken) TableName() string { return "telegram_portal_tokens" } + +// GenerateTelegramPortalToken genera (o renueva) el código de vinculación para el usuario. +// El código tiene 6 caracteres hexadecimales en mayúsculas (e.g. "A3F9B2"). +func GenerateTelegramPortalToken(portalUserID uint) (*TelegramPortalToken, error) { + // Eliminar tokens previos del mismo usuario + app.Http.Database.DB.Where("portal_user_id = ?", portalUserID).Delete(&TelegramPortalToken{}) + + b := make([]byte, 3) + if _, err := rand.Read(b); err != nil { + return nil, fmt.Errorf("no se pudo generar token: %w", err) + } + token := fmt.Sprintf("%06X", b) + + t := &TelegramPortalToken{PortalUserID: portalUserID, Token: token} + if err := app.Http.Database.DB.Create(t).Error; err != nil { + return nil, err + } + return t, nil +} + +// FindTelegramPortalToken busca un token de vinculación activo. +func FindTelegramPortalToken(token string) (*TelegramPortalToken, error) { + var t TelegramPortalToken + err := app.Http.Database.DB.Where("token = ?", token).First(&t).Error + return &t, err +} + +// DeleteTelegramPortalToken elimina el token de un usuario (tras vincular correctamente). +func DeleteTelegramPortalToken(portalUserID uint) { + app.Http.Database.DB.Where("portal_user_id = ?", portalUserID).Delete(&TelegramPortalToken{}) +} diff --git a/pkg/services/telegram_service.go b/pkg/services/telegram_service.go index 7f6812b..cd1ffec 100755 --- a/pkg/services/telegram_service.go +++ b/pkg/services/telegram_service.go @@ -57,6 +57,28 @@ func (ts *TelegramService) SendMessage(chatID interface{}, message string) error return nil } +// GetBotUsername obtiene el username (@nombre) del bot a partir de su token. +func GetBotUsername(botToken string) string { + if botToken == "" { + return "" + } + resp, err := http.Get(fmt.Sprintf("https://api.telegram.org/bot%s/getMe", botToken)) + if err != nil { + return "" + } + defer resp.Body.Close() + var result struct { + OK bool `json:"ok"` + Result struct { + Username string `json:"username"` + } `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "" + } + return result.Result.Username +} + // SendMessageWithToken envía un mensaje usando un bot token explícito (útil para notificar a usuarios con su propio chat_id). func (ts *TelegramService) SendMessageWithToken(chatID interface{}, message, botToken string) error { svc := &TelegramService{BotToken: botToken} diff --git a/resources/views/layouts/portal.html b/resources/views/layouts/portal.html index 77e9614..5277e34 100644 --- a/resources/views/layouts/portal.html +++ b/resources/views/layouts/portal.html @@ -140,6 +140,10 @@ :class="tab==='password' ? 'border-[#8eb02f] text-[#8eb02f]' : 'border-transparent text-slate-500 hover:text-slate-700'"> Contraseña + @@ -218,6 +222,22 @@ + + +
+ +
+ + +
+

+ +

PDF, JPG o PNG · Máx. 10 MB

+
@@ -245,6 +265,92 @@ + +
+ + +
+ +
+

Telegram vinculado

+

Chat ID:

+
+
+
+ +

Aún no has vinculado Telegram. Sigue los pasos para recibir notificaciones.

+
+ + +
+
+

Cómo vincular Telegram

+ + +
+ 1 +
+

Genera tu código

+

Haz clic en el botón para obtener un código único de vinculación.

+ +
+
+ + +
+

Tu código de vinculación:

+

+
+ + +
+ 2 +
+

Envía el código al bot

+

Abre el bot en Telegram y envía el siguiente mensaje:

+
+ + +
+ +
+
+ + +
+ 3 +
+

Verifica la vinculación

+

Después de enviar el mensaje, haz clic en verificar.

+ +

+
+
+ +
+
+ +
+ @@ -263,6 +369,9 @@ Procesando… + @@ -292,16 +401,23 @@ function miCuenta() { open: false, tab: 'perfil', saving: false, - perfil: { nombre:'', email:'', telefono:'', indicativo:'', pais:'', documento:'', empresa:'' }, + perfil: { nombre:'', email:'', telefono:'', indicativo:'', pais:'', documento:'', empresa:'', documento_rut_file:'', documento_rut_nombre:'' }, perfilMsg: '', perfilOk: false, pass: { actual:'', nueva:'', confirma:'' }, passMsg: '', passOk: false, + rutMsg: '', rutOk: false, + tg: { linked: false, chat_id: '', token: '', bot_username: '', bot_link: '', generando: false, verificando: false, verMsg: '', verOk: false }, async init() { try { const r = await axios.get('/portal/mi-perfil'); this.perfil = r.data; } catch(e) {} + try { + const s = await axios.get('/portal/mi-perfil/telegram-status'); + this.tg.linked = s.data.linked; + this.tg.chat_id = s.data.chat_id; + } catch(e) {} }, async guardarPerfil() { @@ -335,6 +451,60 @@ function miCuenta() { this.passOk = false; } finally { this.saving = false; } }, + + async subirRut(event) { + const file = event.target.files[0]; + if (!file) return; + this.rutMsg = ''; + const fd = new FormData(); + fd.append('rut_file', file); + try { + const r = await axios.post('/portal/mi-perfil/rut-file', fd, { headers: { 'Content-Type': 'multipart/form-data' } }); + this.perfil.documento_rut_nombre = r.data.nombre; + this.rutMsg = '✅ Archivo subido correctamente.'; + this.rutOk = true; + } catch(e) { + this.rutMsg = e.response?.data?.error || 'Error al subir archivo.'; + this.rutOk = false; + } + event.target.value = ''; + }, + + async tgGenerarCodigo() { + this.tg.generando = true; + this.tg.token = ''; + this.tg.verMsg = ''; + try { + const r = await axios.post('/portal/mi-perfil/telegram-init'); + this.tg.token = r.data.token; + this.tg.bot_username = r.data.bot_username || ''; + this.tg.bot_link = r.data.bot_link || ''; + } catch(e) { + this.tg.verMsg = 'No se pudo generar el código. Intenta de nuevo.'; + this.tg.verOk = false; + } finally { this.tg.generando = false; } + }, + + async tgVerificar() { + this.tg.verificando = true; + this.tg.verMsg = ''; + try { + const r = await axios.get('/portal/mi-perfil/telegram-status'); + if (r.data.linked) { + this.tg.linked = true; + this.tg.chat_id = r.data.chat_id; + this.tg.token = ''; + this.tg.verMsg = '✅ ¡Telegram vinculado correctamente!'; + this.tg.verOk = true; + } else { + this.tg.verMsg = 'Aún no se detectó el mensaje. Asegúrate de enviar el código al bot.'; + this.tg.verOk = false; + } + } catch(e) { + this.tg.verMsg = 'Error al verificar. Intenta de nuevo.'; + this.tg.verOk = false; + } finally { this.tg.verificando = false; } + }, } } diff --git a/resources/views/portal_usuarios.html b/resources/views/portal_usuarios.html index be802f2..09c6ac0 100644 --- a/resources/views/portal_usuarios.html +++ b/resources/views/portal_usuarios.html @@ -60,6 +60,9 @@ + @@ -147,6 +150,116 @@ + +
+
+
+ + +
+

Detalle del usuario

+ +
+ + +
+

Cargando…

+
+ + +
+ + +
+
+
+

+

+
+
+ + +
+ + + +
+ + +
+
+

Teléfono

+

+
+
+

País

+

+
+
+

Empresa

+

+
+
+

NIT / Documento

+

+
+
+

Telegram Chat ID

+

+
+
+

Notas

+

+
+
+ + +
+

Documento RUT / Cámara de comercio

+
+ + + Descargar +
+
+ + +
+

Accesos de cliente

+
+ + Sin accesos asignados +
+
+ + +
+ Registrado: +
+
+ + +
+ + +
+ +
+
+