diff --git a/migrations/migrate.go b/migrations/migrate.go index 9f23368..0c22366 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -40,6 +40,7 @@ func Migrate() { &models.ProvServidor{}, &models.QrVcard{}, &models.OssApi{}, + &models.VcfVcard{}, ); err != nil { log.Fatalf("Error during main migration: %v", err) } diff --git a/pkg/models/vcard_vcf.go b/pkg/models/vcard_vcf.go new file mode 100755 index 0000000..1f8645e --- /dev/null +++ b/pkg/models/vcard_vcf.go @@ -0,0 +1,71 @@ +package models + +import ( + "log" + + "github.com/sujit-baniya/fiber-boilerplate/app" + "gorm.io/gorm" +) + +type VcfVcard struct { + gorm.Model + Vcf string `json:"vcf" gorm:"column:vcf"` + 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 'vcfvcard' +func (VcfVcard) TableName() string { + return "vcfvcard" +} + +// GetAllVcfVcard obtiene todos los registros de VcfVcard con paginación y búsqueda +func GetAllVcfVcard(limit, offset int, search string) ([]VcfVcard, int64, error) { + var items []VcfVcard + var total int64 + db := app.Http.Database.DB.Model(&VcfVcard{}) + + // 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 VcfVcard: %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 VcfVcard: %v", err) + return nil, 0, err + } + + return items, total, nil +} + +// CreateVcfVcard crea un nuevo registro de VcfVcard en la base de datos +func CreateVcfVcard(vcfVcard *VcfVcard) error { + if err := app.Http.Database.DB.Create(&vcfVcard).Error; err != nil { + return err + } + return nil +} + +// UpdateVcfVcard actualiza un registro de VcfVcard existente +func UpdateVcfVcard(vcfVcard *VcfVcard) error { + if err := app.Http.Database.DB.Model(&vcfVcard).Updates(vcfVcard).Error; err != nil { + return err + } + return nil +} + +// DeleteVcfVcard elimina un registro de VcfVcard de la base de datos +func DeleteVcfVcard(vcfVcard *VcfVcard) error { + if err := app.Http.Database.DB.Delete(&vcfVcard).Error; err != nil { + return err + } + return nil +} diff --git a/pkg/services/vcf_service.go b/pkg/services/vcf_service.go new file mode 100755 index 0000000..a31875e --- /dev/null +++ b/pkg/services/vcf_service.go @@ -0,0 +1,119 @@ +package services + +import ( + "fmt" + "strings" + "time" +) + +// ContactData estructura para la vCard extendida +type ContactDataVcf 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"` +} + + +// GenerateVCardVcf genera el contenido de una vCard extendida como archivo .vcf (sin QR) +func GenerateVCardVcf(data ContactDataVcf) ([]byte, error) { + now := time.Now().UTC().Format("2006-01-02T15:04:05Z") + + var vcardBuilder strings.Builder + vcardBuilder.WriteString("BEGIN:VCARD\n") + vcardBuilder.WriteString("VERSION:3.0\n") + + 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)) + } + 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") + + return []byte(vcardBuilder.String()), nil +} diff --git a/rest/controllers/api/vcard_vcf.go b/rest/controllers/api/vcard_vcf.go new file mode 100755 index 0000000..709bb3b --- /dev/null +++ b/rest/controllers/api/vcard_vcf.go @@ -0,0 +1,107 @@ +package controllers + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "os" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/sujit-baniya/fiber-boilerplate/app" + "github.com/sujit-baniya/fiber-boilerplate/pkg/helpers" + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" + "github.com/sujit-baniya/fiber-boilerplate/pkg/services" + "gorm.io/gorm" + // Importa el repositorio +) + +func CreateVcf(c *fiber.Ctx) error { + var data services.ContactDataVcf + if err := json.Unmarshal(c.Body(), &data); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Datos inválidos", + }) + } + + // 1. Generar el Vcf + vcfContent, err := services.GenerateVCardVcf(data) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "No se pudo generar el VCF", + }) + } + + random := helpers.RandomString(4) + fileName := fmt.Sprintf("vcf/vcf-%s-%s.vcf", strings.ReplaceAll(data.FirstName, " ", "_"), random) + tmpPath := fmt.Sprintf("/tmp/vcf-%s-%s.vcf", strings.ReplaceAll(data.FirstName, " ", "_"), random) + + // 2. Guardar archivo temporal + if err := os.WriteFile(tmpPath, vcfContent, 0644); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "No se pudo guardar el VCF temporalmente", + }) + } + defer os.Remove(tmpPath) + + // 3. Subir a OSS + ossService, err := services.NewOSSServiceFromDB() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error de conexión con OSS", + }) + } + + if err := ossService.UploadFile(fileName, tmpPath); err != nil { + log.Printf("Error subiendo archivo a OSS: %v", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error subiendo archivo a OSS", + }) + } + + // 4. URL del archivo + url := fmt.Sprintf("https://%s.%s/%s", ossService.BucketName, ossService.Endpoint, fileName) + + // 5. Buscar si ya existe un registro con ese vcard_id + var existing models.VcfVcard + db := app.Http.Database.DB + + err = db.Where("vcard_id = ?", data.VcardID).First(&existing).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // No existe, crear nuevo + newVcf := models.VcfVcard{ + Vcf: url, + UsuarioID: data.FirstName, // Podrías usar el ID real de usuario si lo tienes + Email: data.Email, + VcardID: data.VcardID, + } + if err := models.CreateVcfVcard(&newVcf); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "No se pudo crear el registro VCF", + }) + } + } else { + // Error inesperado de DB + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error buscando VCF existente", + }) + } + } else { + // Si ya existe, actualizar + existing.Vcf = url + existing.Email = data.Email + existing.UsuarioID = data.FirstName + if err := models.UpdateVcfVcard(&existing); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "No se pudo actualizar el VCF existente", + }) + } + } + + return c.JSON(fiber.Map{ + "success": true, + "url": url, + }) +} diff --git a/rest/routes/api.go b/rest/routes/api.go index 2077183..019128c 100755 --- a/rest/routes/api.go +++ b/rest/routes/api.go @@ -13,7 +13,8 @@ func v1AuthRoutes(api fiber.Router) { api.Post("/oauth/token", apiControllers.OAuthToken) api.Post("/generate-qr-tmp", apiControllers.CreateQrTmp) api.Post("/generate-qr", apiControllers.CreateQr) - + api.Post("/generate-vcf", apiControllers.CreateVcf) + } func v1Routes(api fiber.Router) {