package controllers import ( "net/http" "os" "path/filepath" "strconv" "strings" "time" database "goGin/app/database/config" "goGin/app/database/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // Payload for creating/updating settings type SettingPayload struct { Title string `json:"title" binding:"required"` MetaTitle string `json:"meta_title" binding:"required"` MetaDescription string `json:"meta_description" binding:"required"` Phone string `json:"phone" binding:"required"` URL string `json:"url" binding:"required"` Email string `json:"email" binding:"required"` Facebook string `json:"facebook"` X string `json:"x"` Instagram string `json:"instagram"` Whatsapp string `json:"whatsapp"` Pinterest string `json:"pinterest"` Linkedin string `json:"linkedin"` Slogan string `json:"slogan"` Address string `json:"address"` Copyright string `json:"copyright"` MapEmbed string `json:"map_embed"` WLogo string `json:"w_logo"` BLogo string `json:"b_logo"` IsActive *bool `json:"is_active"` // Optional image transformation / dimension settings WWidth *int `json:"w_width"` WHeight *int `json:"w_height"` WQuality *int `json:"w_quality"` WFormat string `json:"w_format"` BWidth *int `json:"b_width"` BHeight *int `json:"b_height"` BQuality *int `json:"b_quality"` BFormat string `json:"b_format"` } // AdminListSettings godoc // @Summary Admin: List settings // @Description Admin listing of settings. Use ?soft=only to list deleted, ?soft=with to include deleted. // @Tags settings // @Security BearerAuth // @Produce json // @Param page query int false "Page number" // @Param per_page query int false "Items per page" // @Param soft query string false "Soft delete filter: only|with" // @Success 200 {object} controllers.SettingListResponse // @Failure 500 {object} map[string]string // @Router /api/v1/admin/settings [get] func AdminListSettings(c *gin.Context) { if database.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"}) return } pageStr := c.DefaultQuery("page", "1") perPageStr := c.DefaultQuery("per_page", "20") page, _ := strconv.Atoi(pageStr) perPage, _ := strconv.Atoi(perPageStr) if page < 1 { page = 1 } if perPage < 1 { perPage = 20 } if perPage > 200 { perPage = 200 } offset := (page - 1) * perPage soft := c.Query("soft") var query *gorm.DB if soft == "only" { query = database.DB.Unscoped().Model(&models.Setting{}).Where("deleted_at IS NOT NULL") } else if soft == "with" { query = database.DB.Unscoped().Model(&models.Setting{}) } else { query = database.DB.Model(&models.Setting{}) } var total int64 if err := query.Count(&total).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } var items []models.Setting if err := query.Order("created_at desc").Limit(perPage).Offset(offset).Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"items": items, "total": total, "page": page, "per_page": perPage}) } // AdminGetSetting godoc // @Summary Admin: Get a setting by id // @Description Return a single setting by id // @Tags settings // @Security BearerAuth // @Produce json // @Param id path int true "Setting ID" // @Success 200 {object} controllers.SettingResponse // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/admin/settings/{id} [get] func AdminGetSetting(c *gin.Context) { if database.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"}) return } idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil || id < 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var s models.Setting if err := database.DB.Unscoped().First(&s, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "setting not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": s}) } // AdminCreateSetting godoc // @Summary Admin: Create a setting // @Description Create a new setting // @Tags settings // @Security BearerAuth // @Accept multipart/form-data // @Produce json // @Param title formData string true "Title" // @Param meta_title formData string true "Meta title" // @Param meta_description formData string true "Meta description" // @Param phone formData string true "Phone" // @Param url formData string true "URL" // @Param email formData string true "Email" // @Param facebook formData string false "Facebook" // @Param x formData string false "X" // @Param instagram formData string false "Instagram" // @Param whatsapp formData string false "Whatsapp" // @Param pinterest formData string false "Pinterest" // @Param linkedin formData string false "Linkedin" // @Param slogan formData string false "Slogan" // @Param address formData string false "Address" // @Param copyright formData string false "Copyright" // @Param map_embed formData string false "Map embed" // @Param w_logo formData file false "White logo file upload (or provide w_logo path as string)" // @Param b_logo formData file false "Black logo file upload (or provide b_logo path as string)" // @Param is_active formData boolean false "Is active" // @Param w_width formData int false "W logo width" // @Param w_height formData int false "W logo height" // @Param w_quality formData int false "W logo quality" // @Param w_format formData string false "W logo format" // @Param b_width formData int false "B logo width" // @Param b_height formData int false "B logo height" // @Param b_quality formData int false "B logo quality" // @Param b_format formData string false "B logo format" // @Success 201 {object} controllers.SettingResponse // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/admin/settings [post] func AdminCreateSetting(c *gin.Context) { if database.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"}) return } // Support both JSON and multipart/form-data var payload SettingPayload contentType := c.GetHeader("Content-Type") if strings.HasPrefix(contentType, "multipart/form-data") { // read form fields payload.Title = c.PostForm("title") payload.MetaTitle = c.PostForm("meta_title") payload.MetaDescription = c.PostForm("meta_description") payload.Phone = c.PostForm("phone") payload.URL = c.PostForm("url") payload.Email = c.PostForm("email") payload.Facebook = c.PostForm("facebook") payload.X = c.PostForm("x") payload.Instagram = c.PostForm("instagram") payload.Whatsapp = c.PostForm("whatsapp") payload.Pinterest = c.PostForm("pinterest") payload.Linkedin = c.PostForm("linkedin") payload.Slogan = c.PostForm("slogan") payload.Address = c.PostForm("address") payload.Copyright = c.PostForm("copyright") payload.MapEmbed = c.PostForm("map_embed") // keep payload.WLogo/BLogo as string if client sends path payload.WLogo = c.PostForm("w_logo") payload.BLogo = c.PostForm("b_logo") if v := c.PostForm("is_active"); v != "" { if b, err := strconv.ParseBool(v); err == nil { payload.IsActive = &b } } // numeric metadata if v := c.PostForm("w_width"); v != "" { if n, err := strconv.Atoi(v); err == nil { payload.WWidth = &n } } if v := c.PostForm("w_height"); v != "" { if n, err := strconv.Atoi(v); err == nil { payload.WHeight = &n } } if v := c.PostForm("w_quality"); v != "" { if n, err := strconv.Atoi(v); err == nil { payload.WQuality = &n } } payload.WFormat = c.PostForm("w_format") if v := c.PostForm("b_width"); v != "" { if n, err := strconv.Atoi(v); err == nil { payload.BWidth = &n } } if v := c.PostForm("b_height"); v != "" { if n, err := strconv.Atoi(v); err == nil { payload.BHeight = &n } } if v := c.PostForm("b_quality"); v != "" { if n, err := strconv.Atoi(v); err == nil { payload.BQuality = &n } } payload.BFormat = c.PostForm("b_format") } else { // JSON if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } // basic required validation if payload.Title == "" || payload.MetaTitle == "" || payload.MetaDescription == "" || payload.Phone == "" || payload.URL == "" || payload.Email == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing required fields"}) return } isActive := false if payload.IsActive != nil { isActive = *payload.IsActive } setting := models.Setting{ Title: payload.Title, MetaTitle: payload.MetaTitle, MetaDescription: payload.MetaDescription, Phone: payload.Phone, URL: payload.URL, Email: payload.Email, Facebook: payload.Facebook, X: payload.X, Instagram: payload.Instagram, Whatsapp: payload.Whatsapp, Pinterest: payload.Pinterest, Linkedin: payload.Linkedin, Slogan: payload.Slogan, Address: payload.Address, Copyright: payload.Copyright, MapEmbed: payload.MapEmbed, WLogo: payload.WLogo, BLogo: payload.BLogo, IsActive: isActive, } // optional image transform params if payload.WWidth != nil { setting.WWidth = *payload.WWidth } if payload.WHeight != nil { setting.WHeight = *payload.WHeight } if payload.WQuality != nil { setting.WQuality = *payload.WQuality } setting.WFormat = payload.WFormat if payload.BWidth != nil { setting.BWidth = *payload.BWidth } if payload.BHeight != nil { setting.BHeight = *payload.BHeight } if payload.BQuality != nil { setting.BQuality = *payload.BQuality } setting.BFormat = payload.BFormat // Handle optional logo file uploads when multipart/form-data if strings.HasPrefix(contentType, "multipart/form-data") { // Support file upload on field name 'w_logo' (preferred) or fallback to provided path if file, err := c.FormFile("w_logo"); err == nil { uploadDir := filepath.Join("uploads", "logos") _ = os.MkdirAll(uploadDir, os.ModePerm) ext := filepath.Ext(file.Filename) newName := "wlogo-" + strconv.FormatInt(time.Now().UnixNano(), 10) + ext destination := filepath.Join(uploadDir, newName) if err := c.SaveUploadedFile(file, destination); err == nil { setting.WLogo = "/uploads/logos/" + newName if setting.WFormat == "" && ext != "" { setting.WFormat = ext[1:] } } } // Support file upload on field name 'b_logo' if file, err := c.FormFile("b_logo"); err == nil { uploadDir := filepath.Join("uploads", "logos") _ = os.MkdirAll(uploadDir, os.ModePerm) ext := filepath.Ext(file.Filename) newName := "blogo-" + strconv.FormatInt(time.Now().UnixNano(), 10) + ext destination := filepath.Join(uploadDir, newName) if err := c.SaveUploadedFile(file, destination); err == nil { setting.BLogo = "/uploads/logos/" + newName if setting.BFormat == "" && ext != "" { setting.BFormat = ext[1:] } } } } // Enforce single active setting rule if setting.IsActive { // Deactivate all other settings if err := database.DB.Model(&models.Setting{}).Where("1 = 1").Update("is_active", false).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to deactivate other settings: " + err.Error()}) return } } if err := database.DB.Create(&setting).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"data": setting}) } // AdminUpdateSetting godoc // @Summary Admin: Update a setting // @Description Update an existing setting // @Tags settings // @Security BearerAuth // @Accept multipart/form-data // @Produce json // @Param id path int true "Setting ID" // @Param title formData string false "Title" // @Param meta_title formData string false "Meta title" // @Param meta_description formData string false "Meta description" // @Param phone formData string false "Phone" // @Param url formData string false "URL" // @Param email formData string false "Email" // @Param facebook formData string false "Facebook" // @Param x formData string false "X" // @Param instagram formData string false "Instagram" // @Param whatsapp formData string false "Whatsapp" // @Param pinterest formData string false "Pinterest" // @Param linkedin formData string false "Linkedin" // @Param slogan formData string false "Slogan" // @Param address formData string false "Address" // @Param copyright formData string false "Copyright" // @Param map_embed formData string false "Map embed" // @Param w_logo formData file false "White logo file upload (or provide w_logo path as string)" // @Param b_logo formData file false "Black logo file upload (or provide b_logo path as string)" // @Param is_active formData boolean false "Is active" // @Param w_width formData int false "W logo width" // @Param w_height formData int false "W logo height" // @Param w_quality formData int false "W logo quality" // @Param w_format formData string false "W logo format" // @Param b_width formData int false "B logo width" // @Param b_height formData int false "B logo height" // @Param b_quality formData int false "B logo quality" // @Param b_format formData string false "B logo format" // @Success 200 {object} controllers.SettingResponse // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/admin/settings/{id} [put] func AdminUpdateSetting(c *gin.Context) { if database.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"}) return } idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil || id < 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var s models.Setting if err := database.DB.First(&s, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "setting not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } contentType := c.GetHeader("Content-Type") if strings.HasPrefix(contentType, "multipart/form-data") { // read form fields and update if present if v := c.PostForm("title"); v != "" { s.Title = v } if v := c.PostForm("meta_title"); v != "" { s.MetaTitle = v } if v := c.PostForm("meta_description"); v != "" { s.MetaDescription = v } if v := c.PostForm("phone"); v != "" { s.Phone = v } if v := c.PostForm("url"); v != "" { s.URL = v } if v := c.PostForm("email"); v != "" { s.Email = v } if v := c.PostForm("facebook"); v != "" { s.Facebook = v } if v := c.PostForm("x"); v != "" { s.X = v } if v := c.PostForm("instagram"); v != "" { s.Instagram = v } if v := c.PostForm("whatsapp"); v != "" { s.Whatsapp = v } if v := c.PostForm("pinterest"); v != "" { s.Pinterest = v } if v := c.PostForm("linkedin"); v != "" { s.Linkedin = v } if v := c.PostForm("slogan"); v != "" { s.Slogan = v } if v := c.PostForm("address"); v != "" { s.Address = v } if v := c.PostForm("copyright"); v != "" { s.Copyright = v } if v := c.PostForm("map_embed"); v != "" { s.MapEmbed = v } if v := c.PostForm("w_logo"); v != "" { s.WLogo = v } if v := c.PostForm("b_logo"); v != "" { s.BLogo = v } if v := c.PostForm("is_active"); v != "" { if b, err := strconv.ParseBool(v); err == nil { s.IsActive = b } } if v := c.PostForm("w_width"); v != "" { if n, err := strconv.Atoi(v); err == nil { s.WWidth = n } } if v := c.PostForm("w_height"); v != "" { if n, err := strconv.Atoi(v); err == nil { s.WHeight = n } } if v := c.PostForm("w_quality"); v != "" { if n, err := strconv.Atoi(v); err == nil { s.WQuality = n } } if v := c.PostForm("w_format"); v != "" { s.WFormat = v } if v := c.PostForm("b_width"); v != "" { if n, err := strconv.Atoi(v); err == nil { s.BWidth = n } } if v := c.PostForm("b_height"); v != "" { if n, err := strconv.Atoi(v); err == nil { s.BHeight = n } } if v := c.PostForm("b_quality"); v != "" { if n, err := strconv.Atoi(v); err == nil { s.BQuality = n } } if v := c.PostForm("b_format"); v != "" { s.BFormat = v } // Handle optional file uploads if file, err := c.FormFile("w_logo"); err == nil { uploadDir := filepath.Join("uploads", "logos") _ = os.MkdirAll(uploadDir, os.ModePerm) ext := filepath.Ext(file.Filename) newName := "wlogo-" + strconv.FormatInt(time.Now().UnixNano(), 10) + ext destination := filepath.Join(uploadDir, newName) if err := c.SaveUploadedFile(file, destination); err == nil { s.WLogo = "/uploads/logos/" + newName if s.WFormat == "" && ext != "" { s.WFormat = ext[1:] } } } if file, err := c.FormFile("b_logo"); err == nil { uploadDir := filepath.Join("uploads", "logos") _ = os.MkdirAll(uploadDir, os.ModePerm) ext := filepath.Ext(file.Filename) newName := "blogo-" + strconv.FormatInt(time.Now().UnixNano(), 10) + ext destination := filepath.Join(uploadDir, newName) if err := c.SaveUploadedFile(file, destination); err == nil { s.BLogo = "/uploads/logos/" + newName if s.BFormat == "" && ext != "" { s.BFormat = ext[1:] } } } } else { // JSON payload var payload SettingPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // update fields from payload s.Title = payload.Title s.MetaTitle = payload.MetaTitle s.MetaDescription = payload.MetaDescription s.Phone = payload.Phone s.URL = payload.URL s.Email = payload.Email s.Facebook = payload.Facebook s.X = payload.X s.Instagram = payload.Instagram s.Whatsapp = payload.Whatsapp s.Pinterest = payload.Pinterest s.Linkedin = payload.Linkedin s.Slogan = payload.Slogan s.Address = payload.Address s.Copyright = payload.Copyright s.MapEmbed = payload.MapEmbed s.WLogo = payload.WLogo s.BLogo = payload.BLogo if payload.IsActive != nil { s.IsActive = *payload.IsActive } if payload.WWidth != nil { s.WWidth = *payload.WWidth } if payload.WHeight != nil { s.WHeight = *payload.WHeight } if payload.WQuality != nil { s.WQuality = *payload.WQuality } s.WFormat = payload.WFormat if payload.BWidth != nil { s.BWidth = *payload.BWidth } if payload.BHeight != nil { s.BHeight = *payload.BHeight } if payload.BQuality != nil { s.BQuality = *payload.BQuality } s.BFormat = payload.BFormat } // Enforce single active setting rule if s.IsActive { // Deactivate all other settings except this one if err := database.DB.Model(&models.Setting{}).Where("id != ?", s.ID).Update("is_active", false).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to deactivate other settings: " + err.Error()}) return } } if err := database.DB.Save(&s).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": s}) } // AdminDeleteSetting godoc // @Summary Admin: Delete a setting // @Description Soft-delete a setting by ID // @Tags settings // @Security BearerAuth // @Produce json // @Param id path int true "Setting ID" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/admin/settings/{id} [delete] func AdminDeleteSetting(c *gin.Context) { if database.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"}) return } idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil || id < 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var s models.Setting if err := database.DB.First(&s, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "setting not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := database.DB.Delete(&s).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // attempt to remove logo files if present (safe: only under uploads/) for _, p := range []string{s.WLogo, s.BLogo} { if p == "" { continue } imgPath := strings.TrimPrefix(p, "/") clean := filepath.Clean(imgPath) if strings.HasPrefix(clean, "uploads"+string(os.PathSeparator)) { _ = os.Remove(clean) } } c.JSON(http.StatusOK, gin.H{"message": "setting deleted successfully", "id": s.ID}) } // AdminRestoreSetting godoc // @Summary Admin: Restore a soft-deleted setting // @Description Restore a soft-deleted setting by ID // @Tags settings // @Security BearerAuth // @Produce json // @Param id path int true "Setting ID" // @Success 200 {object} controllers.SettingResponse // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/admin/settings/{id}/restore [post] func AdminRestoreSetting(c *gin.Context) { if database.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"}) return } idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil || id < 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var s models.Setting // Find soft-deleted record using Unscoped if err := database.DB.Unscoped().Where("id = ?", id).First(&s).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "setting not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // If DeletedAt is zero, record is not soft-deleted if s.DeletedAt.Time.IsZero() { c.JSON(http.StatusBadRequest, gin.H{"error": "setting is not deleted"}) return } // Clear deleted_at (restore) using Unscoped Model to allow update on soft-deleted rows res := database.DB.Unscoped().Model(&models.Setting{}).Where("id = ?", id).UpdateColumn("deleted_at", nil) if res.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": res.Error.Error()}) return } if res.RowsAffected == 0 { c.JSON(http.StatusInternalServerError, gin.H{"error": "restore failed (no rows affected)"}) return } // Reload the record in normal scope to ensure DeletedAt is nil in struct if err := database.DB.First(&s, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Enforce single active setting rule if restored setting is active if s.IsActive { if err := database.DB.Model(&models.Setting{}).Where("id != ?", s.ID).Update("is_active", false).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to deactivate other settings: " + err.Error()}) return } } c.JSON(http.StatusOK, gin.H{"data": s}) } // GetSettings godoc // @Summary Public: Get site settings // @Description Return the active site setting (latest active). If none active, return latest setting. // @Tags settings // @Produce json // @Success 200 {object} controllers.SettingResponse // @Failure 500 {object} map[string]string // @Router /api/v1/settings [get] func GetSettings(c *gin.Context) { if database.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"}) return } var s models.Setting // Try to find active setting if err := database.DB.Where("is_active = ?", true).Order("updated_at desc").First(&s).Error; err != nil { // if not found, fallback to latest if err == gorm.ErrRecordNotFound { if err2 := database.DB.Order("updated_at desc").First(&s).Error; err2 != nil { if err2 == gorm.ErrRecordNotFound { c.JSON(http.StatusOK, gin.H{"data": nil}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err2.Error()}) return } } else { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } c.JSON(http.StatusOK, gin.H{"data": s}) }