From 79d6cca94dbd82c51366e76d50e9e48faa79cc93 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 18 Apr 2025 07:38:32 -0500 Subject: [PATCH] qr vcard --- migrations/migrate.go | 2 + pkg/helpers/random.go | 19 ++ pkg/models/conx_ssh.go | 1 + pkg/models/oss_api.go | 88 ++++++++++ pkg/models/users.go | 2 +- pkg/models/vcard_qr.go | 71 ++++++++ pkg/services/oss_service.go | 93 ++++++++++ pkg/services/qr_service.go | 151 ++++++++++++++++ pkg/services/telegram_service.go | 58 +++++++ resources/views/{ => auth}/login.html | 2 +- .../views/auth/request-password-reset.html | 4 +- resources/views/conx_ssh.html | 67 ++++++-- resources/views/ssh.html | 31 ++++ resources/views/token_vencido.html | 2 +- rest/controllers/api/vcard_qr.go | 162 ++++++++++++++++++ rest/controllers/register_controller.go | 27 +-- rest/controllers/telegram_controller.go | 28 +++ rest/middlewares/auth.go | 31 +++- rest/routes/api.go | 3 + rest/routes/auth.go | 12 +- rest/routes/publicas.go | 2 +- rest/routes/routes.go | 5 +- rest/routes/user.go | 33 ++-- 23 files changed, 821 insertions(+), 73 deletions(-) create mode 100644 pkg/helpers/random.go create mode 100644 pkg/models/oss_api.go create mode 100644 pkg/models/vcard_qr.go create mode 100644 pkg/services/oss_service.go create mode 100644 pkg/services/qr_service.go create mode 100644 pkg/services/telegram_service.go rename resources/views/{ => auth}/login.html (98%) create mode 100644 resources/views/ssh.html create mode 100644 rest/controllers/api/vcard_qr.go create mode 100644 rest/controllers/telegram_controller.go diff --git a/migrations/migrate.go b/migrations/migrate.go index 1c9e255..9f23368 100644 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -38,6 +38,8 @@ func Migrate() { &models.ConxDb{}, &models.ConxSsh{}, &models.ProvServidor{}, + &models.QrVcard{}, + &models.OssApi{}, ); err != nil { log.Fatalf("Error during main migration: %v", err) } diff --git a/pkg/helpers/random.go b/pkg/helpers/random.go new file mode 100644 index 0000000..dc2efee --- /dev/null +++ b/pkg/helpers/random.go @@ -0,0 +1,19 @@ +package helpers + +import ( + "math/rand" + "time" +) + +const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) + +// RandomString genera una cadena alfanumérica aleatoria de longitud n +func RandomString(n int) string { + result := make([]byte, n) + for i := range result { + result[i] = charset[seededRand.Intn(len(charset))] + } + return string(result) +} diff --git a/pkg/models/conx_ssh.go b/pkg/models/conx_ssh.go index 125f141..5b866cf 100644 --- a/pkg/models/conx_ssh.go +++ b/pkg/models/conx_ssh.go @@ -12,6 +12,7 @@ type ConxSsh struct { Usuario string `json:"usuario" gorm:"column:usuario"` Puerto string `json:"puerto" gorm:"column:puerto"` ServidorID uint `json:"servidor_id" gorm:"column:servidor_id;foreignKey:ServidorID"` + Password string `json:"password" gorm:"column:password"` Servidor Servidor `json:"servidor" gorm:"foreignKey:ServidorID"` } diff --git a/pkg/models/oss_api.go b/pkg/models/oss_api.go new file mode 100644 index 0000000..286f741 --- /dev/null +++ b/pkg/models/oss_api.go @@ -0,0 +1,88 @@ +package models + +import ( + "log" + + "github.com/sujit-baniya/fiber-boilerplate/app" + "gorm.io/gorm" +) + +type OssApi struct { + gorm.Model + Name string `gorm:"size:100;not null" json:"name"` // Alias o nombre interno + Endpoint string `gorm:"not null" json:"endpoint"` // Endpoint del access point + AccessKeyID string `gorm:"not null" json:"access_key_id"` // Access Key ID + AccessKeySecret string `gorm:"not null" json:"access_key_secret"` // Access Key Secret + BucketName string `gorm:"not null" json:"bucket_name"` // Nombre del bucket o access point + Region string `gorm:"size:50" json:"region"` // Región, opcional + IsActive bool `gorm:"default:true" json:"is_active"` // Activar o desactivar config + Notes string `gorm:"type:text" json:"notes"` +} + +// TableName asegura que GORM use la tabla 'qrvcard' +func (OssApi) TableName() string { + return "oss_api" +} + +// GetAllQrVcard obtiene todos los registros de QrVcard con paginación y búsqueda +func GetAllOssApi(limit, offset int, search string) ([]OssApi, int64, error) { + var items []OssApi + var total int64 + db := app.Http.Database.DB.Model(&OssApi{}) + + // Si se pasa un término de búsqueda, lo aplicamos en la consulta + if search != "" { + db = db.Where("name LIKE ?", "%"+search+"%") + } + + // Contamos el total de registros + if err := db.Count(&total).Error; err != nil { + log.Printf("Error counting OssApi: %v", err) + return nil, 0, err + } + + // Obtenemos los registros con paginación + if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil { + log.Printf("Error retrieving OssApi: %v", err) + return nil, 0, err + } + + return items, total, nil +} + +// CreateQrVcard crea un nuevo registro de QrVcard en la base de datos +func CreateOssApi(OssApi *OssApi) error { + if err := app.Http.Database.DB.Create(&OssApi).Error; err != nil { + return err + } + return nil +} + +// UpdateQrVcard actualiza un registro de QrVcard existente +func UpdateOssApi(OssApi *OssApi) error { + if err := app.Http.Database.DB.Model(&OssApi).Updates(OssApi).Error; err != nil { + return err + } + return nil +} + +// DeleteQrVcard elimina un registro de QrVcard de la base de datos +func DeleteOssApi(OssApi *OssApi) error { + if err := app.Http.Database.DB.Delete(&OssApi).Error; err != nil { + return err + } + return nil +} + +// GetLastActiveOssApi obtiene el último registro activo +func GetLastActiveOssApi() (*OssApi, error) { + var ossConfig OssApi + err := app.Http.Database.DB. + Where("is_active = ?", true). + Order("id DESC"). + First(&ossConfig).Error + if err != nil { + return nil, err + } + return &ossConfig, nil +} diff --git a/pkg/models/users.go b/pkg/models/users.go index 27cc64c..22cb7ca 100644 --- a/pkg/models/users.go +++ b/pkg/models/users.go @@ -191,7 +191,7 @@ func GetUserByUsuario(usuario string) (*Users, error) { fmt.Println(usuario) // Verifica el usuario recibido db := app.Http.Database.DB.Model(&Users{}) // Usamos 1 explícitamente para indicar que el estado debe ser "activo" - if err := db.Where("nombre_usuario = ? AND email_verified = ? AND estado = ?", usuario, true, 1).First(&user).Error; err != nil { + if err := db.Where("nombre_usuario = ? AND email_verified = ? AND estado = ?", usuario, true, true).First(&user).Error; err != nil { return nil, err } diff --git a/pkg/models/vcard_qr.go b/pkg/models/vcard_qr.go new file mode 100644 index 0000000..892e055 --- /dev/null +++ b/pkg/models/vcard_qr.go @@ -0,0 +1,71 @@ +package models + +import ( + "log" + + "github.com/sujit-baniya/fiber-boilerplate/app" + "gorm.io/gorm" +) + +type QrVcard struct { + gorm.Model + Qr string `json:"Qr" gorm:"column:qr"` + UsuarioID string `json:"usuario" gorm:"column:usuario"` + Email string `json:"email" gorm:"column:email"` + VcardID string `json:"vcard_id" gorm:"column:vcard_id"` +} + +// TableName asegura que GORM use la tabla 'qrvcard' +func (QrVcard) TableName() string { + return "qrvcard" +} + +// GetAllQrVcard obtiene todos los registros de QrVcard con paginación y búsqueda +func GetAllQrVcard(limit, offset int, search string) ([]QrVcard, int64, error) { + var items []QrVcard + var total int64 + db := app.Http.Database.DB.Model(&QrVcard{}) + + // Si se pasa un término de búsqueda, lo aplicamos en la consulta + if search != "" { + db = db.Where("email LIKE ?", "%"+search+"%") + } + + // Contamos el total de registros + if err := db.Count(&total).Error; err != nil { + log.Printf("Error counting qrvcard: %v", err) + return nil, 0, err + } + + // Obtenemos los registros con paginación + if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil { + log.Printf("Error retrieving qrvcard: %v", err) + return nil, 0, err + } + + return items, total, nil +} + +// CreateQrVcard crea un nuevo registro de QrVcard en la base de datos +func CreateQrVcard(qrVcard *QrVcard) error { + if err := app.Http.Database.DB.Create(&qrVcard).Error; err != nil { + return err + } + return nil +} + +// UpdateQrVcard actualiza un registro de QrVcard existente +func UpdateQrVcard(qrVcard *QrVcard) error { + if err := app.Http.Database.DB.Model(&qrVcard).Updates(qrVcard).Error; err != nil { + return err + } + return nil +} + +// DeleteQrVcard elimina un registro de QrVcard de la base de datos +func DeleteQrVcard(qrVcard *QrVcard) error { + if err := app.Http.Database.DB.Delete(&qrVcard).Error; err != nil { + return err + } + return nil +} diff --git a/pkg/services/oss_service.go b/pkg/services/oss_service.go new file mode 100644 index 0000000..e72f9b0 --- /dev/null +++ b/pkg/services/oss_service.go @@ -0,0 +1,93 @@ +package services + +import ( + "fmt" + "log" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" +) + +type OSSService struct { + Client *oss.Client + Bucket *oss.Bucket + BucketName string + Endpoint string +} + +// Configuración del OSS +type OSSConfig struct { + Endpoint string + AccessKeyID string + AccessKeySecret string + BucketName string +} + +// Constructor del servicio OSS +func NewOSSService(cfg OSSConfig) (*OSSService, error) { + client, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret) + if err != nil { + return nil, fmt.Errorf("error creando cliente OSS: %w", err) + } + + bucket, err := client.Bucket(cfg.BucketName) + if err != nil { + return nil, fmt.Errorf("error obteniendo bucket: %w", err) + } + + return &OSSService{ + Client: client, + Bucket: bucket, + BucketName: cfg.BucketName, + Endpoint: cfg.Endpoint, + }, nil +} + +// Subir un archivo +func (s *OSSService) UploadFile(objectKey string, filePath string) error { + err := s.Bucket.PutObjectFromFile(objectKey, filePath) + if err != nil { + return fmt.Errorf("error subiendo archivo: %w", err) + } + log.Printf("Archivo '%s' subido como '%s'", filePath, objectKey) + return nil +} + +// Listar archivos en el bucket +func (s *OSSService) ListFiles() ([]string, error) { + objects, err := s.Bucket.ListObjects() + if err != nil { + return nil, fmt.Errorf("error listando archivos: %w", err) + } + + var keys []string + for _, obj := range objects.Objects { + keys = append(keys, obj.Key) + } + return keys, nil +} + +// NewOSSServiceFromDB construye OSSService desde el último registro activo +func NewOSSServiceFromDB() (*OSSService, error) { + cfg, err := models.GetLastActiveOssApi() + if err != nil { + return nil, fmt.Errorf("error obteniendo configuración OSS activa: %w", err) + } + + return NewOSSService(OSSConfig{ + Endpoint: cfg.Endpoint, + AccessKeyID: cfg.AccessKeyID, + AccessKeySecret: cfg.AccessKeySecret, + BucketName: cfg.BucketName, + }) +} + +// Eliminar un archivo del bucket +func (s *OSSService) DeleteFile(objectKey string) error { + err := s.Bucket.DeleteObject(objectKey) + if err != nil { + return fmt.Errorf("error eliminando archivo '%s' de OSS: %w", objectKey, err) + } + log.Printf("Archivo eliminado de OSS: %s", objectKey) + return nil +} diff --git a/pkg/services/qr_service.go b/pkg/services/qr_service.go new file mode 100644 index 0000000..24f0c5f --- /dev/null +++ b/pkg/services/qr_service.go @@ -0,0 +1,151 @@ +package services + +import ( + "bytes" + "fmt" + "image/color" + "image/png" + "strings" + "time" + + "github.com/skip2/go-qrcode" +) + +// ContactData estructura para la vCard extendida +type ContactData struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Nickname string `json:"nickname"` + Phone string `json:"phone"` + Email string `json:"email"` + Org string `json:"org"` + Title string `json:"title"` + Role string `json:"role"` + Website string `json:"website"` + PhotoURL string `json:"photo_url"` + Address string `json:"address"` // Ejemplo: ";;Calle 123;Ciudad;Estado;12345;País" + Birthday string `json:"birthday"` // Formato: "1990-01-01" + Gender string `json:"gender"` // "M", "F", "O", etc. + UID string `json:"uid"` // UUID o identificador único + Categories string `json:"categories"` // Ejemplo: "Amigos,Trabajo" + Note string `json:"note"` + IMPP string `json:"impp"` // Ejemplo: "aim:usuario" + Timezone string `json:"timezone"` // Ejemplo: "-0500" + Language string `json:"language"` // Ejemplo: "es" + ColorHex string `json:"color"` + BgColor string `json:"bg_color"` + Instagram string `json:"instagram"` + Facebook string `json:"facebook"` + TikTok string `json:"tiktok"` + LinkedIn string `json:"linkedin"` + VcardID string `json:"vcard_id"` +} + +// hexToRGBA convierte "#rrggbb" a color.RGBA +func hexToRGBA(hex string) color.RGBA { + var r, g, b uint8 + fmt.Sscanf(hex, "#%02x%02x%02x", &r, &g, &b) + return color.RGBA{r, g, b, 255} +} + +// GenerateVCardQR genera un QR con una vCard extendida incluyendo redes sociales +func GenerateVCardQR(data ContactData) ([]byte, error) { + now := time.Now().UTC().Format("2006-01-02T15:04:05Z") + + // Usamos strings.Builder para construir dinámicamente la vCard + var vcardBuilder strings.Builder + vcardBuilder.WriteString("BEGIN:VCARD\n") + vcardBuilder.WriteString("VERSION:3.0\n") + + // N y FN son obligatorios para una vCard válida + vcardBuilder.WriteString(fmt.Sprintf("N:%s;%s;;;\n", data.LastName, data.FirstName)) + vcardBuilder.WriteString(fmt.Sprintf("FN:%s %s\n", data.FirstName, data.LastName)) + + if data.Nickname != "" { + vcardBuilder.WriteString(fmt.Sprintf("NICKNAME:%s\n", data.Nickname)) + } + if data.Org != "" { + vcardBuilder.WriteString(fmt.Sprintf("ORG:%s\n", data.Org)) + } + if data.Title != "" { + vcardBuilder.WriteString(fmt.Sprintf("TITLE:%s\n", data.Title)) + } + if data.Role != "" { + vcardBuilder.WriteString(fmt.Sprintf("ROLE:%s\n", data.Role)) + } + if data.Phone != "" { + vcardBuilder.WriteString(fmt.Sprintf("TEL;TYPE=CELL:%s\n", data.Phone)) + } + if data.Email != "" { + vcardBuilder.WriteString(fmt.Sprintf("EMAIL:%s\n", data.Email)) + } + if data.Website != "" { + vcardBuilder.WriteString(fmt.Sprintf("URL:%s\n", data.Website)) + } + if data.PhotoURL != "" { + vcardBuilder.WriteString(fmt.Sprintf("PHOTO;VALUE=URI:%s\n", data.PhotoURL)) + } + if data.Address != "" { + vcardBuilder.WriteString(fmt.Sprintf("ADR:%s\n", data.Address)) + } + if data.Birthday != "" { + vcardBuilder.WriteString(fmt.Sprintf("BDAY:%s\n", data.Birthday)) + } + if data.Gender != "" { + vcardBuilder.WriteString(fmt.Sprintf("GENDER:%s\n", data.Gender)) + } + if data.UID != "" { + vcardBuilder.WriteString(fmt.Sprintf("UID:%s\n", data.UID)) + } + if data.Note != "" { + vcardBuilder.WriteString(fmt.Sprintf("NOTE:%s\n", data.Note)) + } + if data.Categories != "" { + vcardBuilder.WriteString(fmt.Sprintf("CATEGORIES:%s\n", data.Categories)) + } + if data.IMPP != "" { + vcardBuilder.WriteString(fmt.Sprintf("IMPP:%s\n", data.IMPP)) + } + if data.Timezone != "" { + vcardBuilder.WriteString(fmt.Sprintf("TZ:%s\n", data.Timezone)) + } + if data.Language != "" { + vcardBuilder.WriteString(fmt.Sprintf("LANG:%s\n", data.Language)) + } + + // Agregando las redes sociales + if data.Instagram != "" { + vcardBuilder.WriteString(fmt.Sprintf("X-INSTAGRAM:%s\n", data.Instagram)) + } + if data.Facebook != "" { + vcardBuilder.WriteString(fmt.Sprintf("X-FACEBOOK:%s\n", data.Facebook)) + } + if data.TikTok != "" { + vcardBuilder.WriteString(fmt.Sprintf("X-TIKTOK:%s\n", data.TikTok)) + } + if data.LinkedIn != "" { + vcardBuilder.WriteString(fmt.Sprintf("X-LINKEDIN:%s\n", data.LinkedIn)) + } + + vcardBuilder.WriteString(fmt.Sprintf("REV:%s\n", now)) + vcardBuilder.WriteString("END:VCARD") + + vcard := vcardBuilder.String() + + qr, err := qrcode.New(vcard, qrcode.Medium) + if err != nil { + return nil, err + } + + qr.ForegroundColor = hexToRGBA(data.ColorHex) + qr.BackgroundColor = hexToRGBA(data.BgColor) + + img := qr.Image(300) + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/pkg/services/telegram_service.go b/pkg/services/telegram_service.go new file mode 100644 index 0000000..98efdfd --- /dev/null +++ b/pkg/services/telegram_service.go @@ -0,0 +1,58 @@ +package services + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" +) + +type TelegramService struct { + BotToken string +} + +type TelegramMessage struct { + ChatID interface{} `json:"chat_id"` + Text string `json:"text"` + ParseMode string `json:"parse_mode"` +} + +func NewTelegramService() *TelegramService { + botToken := os.Getenv("TELEGRAM_BOT_TOKEN") + if botToken == "" { + fmt.Println("Warning: TELEGRAM_BOT_TOKEN not set in environment variables") + } + return &TelegramService{BotToken: botToken} +} + +func (ts *TelegramService) SendMessage(chatID interface{}, message string) error { + if ts.BotToken == "" { + return fmt.Errorf("Telegram bot token is not configured") + } + + apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", ts.BotToken) + + telegramMessage := TelegramMessage{ + ChatID: chatID, + Text: message, + ParseMode: "HTML", + } + + jsonData, err := json.Marshal(telegramMessage) + if err != nil { + return fmt.Errorf("error marshalling request body: %v", err) + } + + resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("error sending request to Telegram API: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("received non-OK response from Telegram API: %d", resp.StatusCode) + } + + return nil +} diff --git a/resources/views/login.html b/resources/views/auth/login.html similarity index 98% rename from resources/views/login.html rename to resources/views/auth/login.html index aca88ab..dd80594 100644 --- a/resources/views/login.html +++ b/resources/views/auth/login.html @@ -28,7 +28,7 @@
diff --git a/resources/views/auth/request-password-reset.html b/resources/views/auth/request-password-reset.html index f106e1b..ce8aa40 100644 --- a/resources/views/auth/request-password-reset.html +++ b/resources/views/auth/request-password-reset.html @@ -1,8 +1,8 @@