first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:46:42 +03:00
commit 2a5b661443
202 changed files with 49770 additions and 0 deletions

View File

@@ -0,0 +1,615 @@
package controllers
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
database "goGin/app/database/config"
"goGin/app/database/models"
"goGin/app/middlewares"
"goGin/app/services"
configs "goGin/config"
utils "goGin/pkg/utis"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
"golang.org/x/oauth2/google"
"gorm.io/gorm"
)
// AuthResponse
type AuthResponse struct {
User UserResponse `json:"user"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// RegisterPayload
type RegisterPayload struct {
UserName string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
// LoginPayload
type LoginPayload struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
// RefreshPayload
type RefreshPayload struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
// Helper to generate secure token for email verification
func generateSecureToken() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
// Register godoc
// @Summary Register a new user
// @Description Register a new user. Sends verification email. Does NOT return tokens.
// @Tags auth
// @Accept json
// @Produce json
// @Param register body RegisterPayload true "Register payload"
// @Success 201 {object} controllers.AuthResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/auth/register [post]
func Register(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
var payload RegisterPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check existing email
var existing models.User
if err := database.DB.Where("email = ?", payload.Email).First(&existing).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "email already registered"})
return
}
hashedPwd, err := utils.HashPassword(payload.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"})
return
}
// Email Verification Token
verificationToken := generateSecureToken()
emailVerified := false
user := models.User{
UserName: payload.UserName,
Email: payload.Email,
Password: hashedPwd,
EmailVerified: &emailVerified,
EmailVerifyToken: verificationToken,
}
if err := database.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Send Verification Email
go func() {
if err := utils.SendVerificationEmail(user.Email, verificationToken); err != nil {
fmt.Printf("Failed to send verification email to %s: %v\n", user.Email, err)
}
}()
// Response
c.JSON(http.StatusCreated, gin.H{
"message": "Registration successful. Please check your email to verify your account.",
"user": toUserResponse(user),
})
}
// VerifyEmail godoc
// @Summary Verify email address
// @Description Verify email using token
// @Tags auth
// @Accept json
// @Produce json
// @Param token query string true "Verification Token"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/auth/verify-email [get]
func VerifyEmail(c *gin.Context) {
token := c.Query("token")
if token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"})
return
}
var user models.User
if err := database.DB.Where("email_verify_token = ?", token).First(&user).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid or expired token"})
return
}
now := time.Now()
verified := true
user.EmailVerified = &verified
user.EmailVerifiedAt = &now
user.EmailVerifyToken = "" // Clear token
if err := database.DB.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to verify email"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Email verified successfully"})
}
// Login godoc
// @Summary Login user
// @Description Login with email and password, returns tokens
// @Tags auth
// @Accept json
// @Produce json
// @Param login body LoginPayload true "Login payload"
// @Success 200 {object} controllers.AuthResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/login [post]
func Login(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
var payload LoginPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := database.DB.Where("email = ?", payload.Email).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !utils.CheckPasswordHash(payload.Password, user.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
// Check if email is verified
if user.EmailVerified != nil && !*user.EmailVerified {
c.JSON(http.StatusUnauthorized, gin.H{"error": "email not verified"})
return
}
isAdmin := false
if user.IsAdmin != nil && *user.IsAdmin {
isAdmin = true
}
jwtService := services.NewJWTService()
accessToken, err := jwtService.GenerateToken(user.ID, isAdmin)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate access token"})
return
}
refreshToken, err := jwtService.GenerateRefreshToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate refresh token"})
return
}
c.JSON(http.StatusOK, gin.H{
"user": toUserResponse(user),
"access_token": accessToken,
"refresh_token": refreshToken,
})
}
// Refresh godoc
// @Summary Refresh access token
// @Description usage: send refresh token to get new access token and refresh token
// @Tags auth
// @Accept json
// @Produce json
// @Param refresh body RefreshPayload true "Refresh token payload"
// @Success 200 {object} map[string]string "Returns both access_token and refresh_token"
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/refresh [post]
func Refresh(c *gin.Context) {
var payload RefreshPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
jwtService := services.NewJWTService()
claims, err := jwtService.ValidateToken(payload.RefreshToken)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})
return
}
if claims.TokenType != "refresh" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "not a refresh token"})
return
}
// Get User
var userID uint
switch v := claims.UserID.(type) {
case float64:
userID = uint(v)
case uint:
userID = v
default:
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user claim"})
return
}
var user models.User
if err := database.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
return
}
isAdmin := false
if user.IsAdmin != nil && *user.IsAdmin {
isAdmin = true
}
newAccessToken, err := jwtService.GenerateToken(user.ID, isAdmin)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
return
}
newRefreshToken, err := jwtService.GenerateRefreshToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate refresh token"})
return
}
c.JSON(http.StatusOK, gin.H{
"access_token": newAccessToken,
"refresh_token": newRefreshToken,
})
}
// Me godoc
// @Summary Get current user (me)
// @Description Get current authenticated user information
// @Tags auth
// @Security BearerAuth
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/auth/me [get]
func Me(c *gin.Context) {
claims, ok := middlewares.GetAuthClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var userID uint
switch v := claims.UserID.(type) {
case float64:
userID = uint(v)
case uint:
userID = v
default:
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user claim"})
return
}
var user models.User
if err := database.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
isAdmin := false
if user.IsAdmin != nil && *user.IsAdmin {
isAdmin = true
}
isVerified := false
if user.EmailVerified != nil && *user.EmailVerified {
isVerified = true
}
// Frontend'in beklediği formata göre response döndür
c.JSON(http.StatusOK, gin.H{
"id": user.ID,
"username": user.UserName,
"email": user.Email,
"email_verified": isVerified,
"is_admin": isAdmin,
})
}
// OAuth Helpers
var (
googleOauthConfig = &oauth2.Config{
RedirectURL: "", // Will be set in init or handler
ClientID: "",
ClientSecret: "",
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
Endpoint: google.Endpoint,
}
githubOauthConfig = &oauth2.Config{
RedirectURL: "",
ClientID: "",
ClientSecret: "",
Scopes: []string{"user:email"},
Endpoint: github.Endpoint,
}
)
func getGoogleConfig() *oauth2.Config {
googleOauthConfig.ClientID = configs.AppConfig.GoogleClientID
googleOauthConfig.ClientSecret = configs.AppConfig.GoogleClientSecret
googleOauthConfig.RedirectURL = configs.AppConfig.GoogleRedirectURL
return googleOauthConfig
}
func getGithubConfig() *oauth2.Config {
githubOauthConfig.ClientID = configs.AppConfig.GithubClientID
githubOauthConfig.ClientSecret = configs.AppConfig.GithubClientSecret
githubOauthConfig.RedirectURL = configs.AppConfig.GithubRedirectURL
return githubOauthConfig
}
// GoogleLogin godoc
// @Summary Google OAuth2 Login
// @Description Redirects to Google for authentication
// @Tags auth
// @Success 302
// @Router /api/v1/auth/google [get]
func GoogleLogin(c *gin.Context) {
url := getGoogleConfig().AuthCodeURL("state_google", oauth2.AccessTypeOffline)
c.Redirect(http.StatusTemporaryRedirect, url)
}
// GoogleCallback godoc
// @Summary Google OAuth2 Callback
// @Description Handles Google OAuth2 callback
// @Tags auth
// @Success 200 {object} controllers.AuthResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/auth/google/callback [get]
func GoogleCallback(c *gin.Context) {
code := c.Query("code")
token, err := getGoogleConfig().Exchange(context.Background(), code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token: " + err.Error()})
return
}
client := getGoogleConfig().Client(context.Background(), token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info: " + err.Error()})
return
}
defer resp.Body.Close()
userData, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read user info"})
return
}
var googleUser struct {
ID string `json:"id"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
Name string `json:"name"`
Picture string `json:"picture"`
}
if err := json.Unmarshal(userData, &googleUser); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse user info"})
return
}
handleSocialLogin(c, "google", googleUser.ID, googleUser.Email, googleUser.Name, googleUser.Picture)
}
// GithubLogin godoc
// @Summary GitHub OAuth2 Login
// @Description Redirects to GitHub for authentication
// @Tags auth
// @Success 302
// @Router /api/v1/auth/github [get]
func GithubLogin(c *gin.Context) {
url := getGithubConfig().AuthCodeURL("state_github", oauth2.AccessTypeOffline)
c.Redirect(http.StatusTemporaryRedirect, url)
}
// GithubCallback godoc
// @Summary GitHub OAuth2 Callback
// @Description Handles GitHub OAuth2 callback
// @Tags auth
// @Success 200 {object} controllers.AuthResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/auth/github/callback [get]
func GithubCallback(c *gin.Context) {
code := c.Query("code")
token, err := getGithubConfig().Exchange(context.Background(), code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token: " + err.Error()})
return
}
client := getGithubConfig().Client(context.Background(), token)
resp, err := client.Get("https://api.github.com/user")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info: " + err.Error()})
return
}
defer resp.Body.Close()
userData, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read user info"})
return
}
var githubUser struct {
ID int64 `json:"id"`
Login string `json:"login"`
Name string `json:"name"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
}
if err := json.Unmarshal(userData, &githubUser); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse user info"})
return
}
// GitHub email might be private, need to fetch separately if empty
email := githubUser.Email
if email == "" {
// Fetch emails
emailResp, err := client.Get("https://api.github.com/user/emails")
if err == nil {
defer emailResp.Body.Close()
var emails []struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
if body, err := io.ReadAll(emailResp.Body); err == nil {
json.Unmarshal(body, &emails)
for _, e := range emails {
if e.Primary && e.Verified {
email = e.Email
break
}
}
}
}
}
if email == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Could not retrieve email from GitHub"})
return
}
handleSocialLogin(c, "github", fmt.Sprintf("%d", githubUser.ID), email, githubUser.Name, githubUser.AvatarURL)
}
func handleSocialLogin(c *gin.Context, provider, providerID, email, name, avatarURL string) {
var user models.User
var socialAccount models.SocialAccount
// Check if social account exists
err := database.DB.Where("provider = ? AND provider_id = ?", provider, providerID).First(&socialAccount).Error
if err == nil {
// Found social account, find user
if err := database.DB.First(&user, socialAccount.UserID).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "User record missing for social account"})
return
}
} else {
// Social account not found. Check if email exists
if err := database.DB.Where("email = ?", email).First(&user).Error; err == nil {
// User exists, add social account
newSocial := models.SocialAccount{
UserID: uint64(user.ID),
Provider: provider,
ProviderID: providerID,
Email: email,
Name: name,
AvatarURL: avatarURL,
}
database.DB.Create(&newSocial)
} else {
// Create new user
verified := true
now := time.Now()
// Generate random password
randomPass := generateSecureToken()
hashedPwd, _ := utils.HashPassword(randomPass)
user = models.User{
UserName: name, // Handle duplicate usernames?
Email: email,
Password: hashedPwd,
EmailVerified: &verified,
EmailVerifiedAt: &now,
}
// Fallback username if empty
if user.UserName == "" {
user.UserName = strings.Split(email, "@")[0]
}
if err := database.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user: " + err.Error()})
return
}
newSocial := models.SocialAccount{
UserID: uint64(user.ID),
Provider: provider,
ProviderID: providerID,
Email: email,
Name: name,
AvatarURL: avatarURL,
}
database.DB.Create(&newSocial)
}
}
// Login logic
isAdmin := false
if user.IsAdmin != nil && *user.IsAdmin {
isAdmin = true
}
jwtService := services.NewJWTService()
accessToken, err := jwtService.GenerateToken(user.ID, isAdmin)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate access token"})
return
}
refreshToken, err := jwtService.GenerateRefreshToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate refresh token"})
return
}
c.JSON(http.StatusOK, gin.H{
"user": toUserResponse(user),
"access_token": accessToken,
"refresh_token": refreshToken,
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,538 @@
package controllers
import (
"fmt"
"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"
)
// Hero payload
type HeroPayload struct {
Color string `json:"color" binding:"required"`
Title string `json:"title"`
Text1 string `json:"text1"`
Text2 string `json:"text2"`
Text4 string `json:"text4"`
Text5 string `json:"text5"`
Image string `json:"image"`
IsActive *bool `json:"is_active"`
}
// AdminListHeroes godoc
// @Summary Admin: List heroes
// @Description Admin listing of heroes. Use ?soft=only to list deleted, ?soft=with to include deleted.
// @Tags heroes
// @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.HeroListResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/heroes [get]
func AdminListHeroes(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.Hero{}).Where("deleted_at IS NOT NULL")
} else if soft == "with" {
query = database.DB.Unscoped().Model(&models.Hero{})
} else {
query = database.DB.Model(&models.Hero{})
}
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []models.Hero
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})
}
// AdminGetHero godoc
// @Summary Admin: Get a hero by id
// @Description Return a single hero by id
// @Tags heroes
// @Security BearerAuth
// @Produce json
// @Param id path int true "Hero ID"
// @Success 200 {object} controllers.HeroResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/heroes/{id} [get]
func AdminGetHero(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 h models.Hero
if err := database.DB.Unscoped().First(&h, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "hero not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": h})
}
// CreateHero godoc
// @Summary Admin: Create a hero
// @Description Create a new hero item (multipart/form-data)
// @Tags heroes
// @Security BearerAuth
// @Accept multipart/form-data
// @Produce json
// @Param color formData string true "Color"
// @Param title formData string false "Title"
// @Param text1 formData string false "Text1"
// @Param text2 formData string false "Text2"
// @Param text4 formData string false "Text4"
// @Param text5 formData string false "Text5"
// @Param is_active formData boolean false "Is Active"
// @Param width formData int false "Image width (frontend-provided)"
// @Param height formData int false "Image height (frontend-provided)"
// @Param quality formData int false "Image quality (frontend-provided)"
// @Param format formData string false "Image format (jpeg|png|webp) (frontend-provided)"
// @Param image formData file false "Image file"
// @Success 201 {object} controllers.HeroResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/heroes [post]
func CreateHero(c *gin.Context) {
if database.DB == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"})
return
}
// Parse form fields
color := c.PostForm("color")
if color == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "color is required"})
return
}
title := c.PostForm("title")
text1 := c.PostForm("text1")
text2 := c.PostForm("text2")
text4 := c.PostForm("text4")
text5 := c.PostForm("text5")
isActive := true
if v := c.PostForm("is_active"); v != "" {
if b, err := strconv.ParseBool(v); err == nil {
isActive = b
}
}
// optional frontend-provided image metadata
var width, height, quality int
if w := c.PostForm("width"); w != "" {
if wi, err := strconv.Atoi(w); err == nil {
width = wi
}
}
if h := c.PostForm("height"); h != "" {
if hi, err := strconv.Atoi(h); err == nil {
height = hi
}
}
if q := c.PostForm("quality"); q != "" {
if qi, err := strconv.Atoi(q); err == nil {
quality = qi
}
}
format := c.PostForm("format")
hero := models.Hero{
Color: color,
Title: title,
Text1: text1,
Text2: text2,
Text4: text4,
Text5: text5,
IsActive: isActive,
Width: width,
Height: height,
Quality: quality,
Format: format,
}
// handle file upload (no server-side image processing)
file, err := c.FormFile("image")
if err == nil {
// ensure uploads/heroes exists
uploadDir := filepath.Join("uploads", "heroes")
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create upload dir"})
return
}
ext := filepath.Ext(file.Filename)
newName := fmt.Sprintf("hero-%d%s", time.Now().UnixNano(), ext)
destination := filepath.Join(uploadDir, newName)
if err := c.SaveUploadedFile(file, destination); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
hero.Image = "/uploads/heroes/" + newName
// do not attempt to decode/process image here; frontend provides metadata
// if format not provided, fallback to extension without dot
if heroFormat := format; heroFormat == "" {
if ext != "" {
hero.Format = ext[1:]
}
}
}
if err := database.DB.Create(&hero).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": hero})
}
// UpdateHero godoc
// @Summary Admin: Update a hero
// @Description Update an existing hero (multipart/form-data)
// @Tags heroes
// @Security BearerAuth
// @Accept multipart/form-data
// @Produce json
// @Param id path int true "Hero ID"
// @Param color formData string false "Color"
// @Param title formData string false "Title"
// @Param text1 formData string false "Text1"
// @Param text2 formData string false "Text2"
// @Param text4 formData string false "Text4"
// @Param text5 formData string false "Text5"
// @Param is_active formData boolean false "Is Active"
// @Param width formData int false "Image width (frontend-provided)"
// @Param height formData int false "Image height (frontend-provided)"
// @Param quality formData int false "Image quality (frontend-provided)"
// @Param format formData string false "Image format (jpeg|png|webp) (frontend-provided)"
// @Param image formData file false "Image file"
// @Success 200 {object} controllers.HeroResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/heroes/{id} [put]
func UpdateHero(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 h models.Hero
if err := database.DB.First(&h, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "hero not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// read form fields (if present)
if color := c.PostForm("color"); color != "" {
h.Color = color
}
if title := c.PostForm("title"); title != "" {
h.Title = title
}
if t := c.PostForm("text1"); t != "" {
h.Text1 = t
}
if t := c.PostForm("text2"); t != "" {
h.Text2 = t
}
if t := c.PostForm("text4"); t != "" {
h.Text4 = t
}
if t := c.PostForm("text5"); t != "" {
h.Text5 = t
}
if v := c.PostForm("is_active"); v != "" {
if b, err := strconv.ParseBool(v); err == nil {
h.IsActive = b
}
}
// optional frontend-provided image metadata
if w := c.PostForm("width"); w != "" {
if wi, err := strconv.Atoi(w); err == nil {
h.Width = wi
}
}
if hgt := c.PostForm("height"); hgt != "" {
if hi, err := strconv.Atoi(hgt); err == nil {
h.Height = hi
}
}
if q := c.PostForm("quality"); q != "" {
if qi, err := strconv.Atoi(q); err == nil {
h.Quality = qi
}
}
if fmtStr := c.PostForm("format"); fmtStr != "" {
h.Format = fmtStr
}
// handle optional file upload (no server-side processing)
file, err := c.FormFile("image")
if err == nil {
// Save new file first
uploadDir := filepath.Join("uploads", "heroes")
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create upload dir"})
return
}
ext := filepath.Ext(file.Filename)
newName := fmt.Sprintf("hero-%d%s", time.Now().UnixNano(), ext)
destination := filepath.Join(uploadDir, newName)
if err := c.SaveUploadedFile(file, destination); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// If there was a previous image, attempt to remove it safely
prev := h.Image
if prev != "" {
// normalize and ensure it's inside uploads/
prevPath := strings.TrimPrefix(prev, "/")
clean := filepath.Clean(prevPath)
// only remove files under uploads/ to avoid accidental deletions
if strings.HasPrefix(clean, "uploads"+string(os.PathSeparator)) {
_ = os.Remove(clean) // ignore error
}
}
h.Image = "/uploads/heroes/" + newName
// if format not provided by frontend, fallback to extension
if h.Format == "" && ext != "" {
h.Format = ext[1:]
}
}
if err := database.DB.Save(&h).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": h})
}
// DeleteHero godoc
// @Summary Admin: Delete a hero
// @Description Soft-delete a hero by ID
// @Tags heroes
// @Security BearerAuth
// @Produce json
// @Param id path int true "Hero 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/heroes/{id} [delete]
func DeleteHero(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 h models.Hero
if err := database.DB.First(&h, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "hero not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := database.DB.Delete(&h).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// attempt to remove image file if present
if h.Image != "" {
imgPath := strings.TrimPrefix(h.Image, "/")
clean := filepath.Clean(imgPath)
if strings.HasPrefix(clean, "uploads"+string(os.PathSeparator)) {
_ = os.Remove(clean)
}
}
c.JSON(http.StatusOK, gin.H{"message": "hero deleted successfully", "id": h.ID})
}
// RestoreHero godoc
// @Summary Admin: Restore a soft-deleted hero
// @Description Restore a soft-deleted hero by ID
// @Tags heroes
// @Security BearerAuth
// @Produce json
// @Param id path int true "Hero ID"
// @Success 200 {object} controllers.HeroResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/heroes/{id}/restore [post]
func RestoreHero(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 h models.Hero
// Find soft-deleted record with Unscoped
if err := database.DB.Unscoped().Where("id = ?", id).First(&h).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "hero not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Clear deleted_at (restore)
if err := database.DB.Unscoped().Model(&models.Hero{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Reload the record in normal scope to ensure DeletedAt is nil in struct
if err := database.DB.First(&h, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": h})
}
// ListHeroes godoc
// @Summary Public: List heroes
// @Description Return active heroes with pagination
// @Tags heroes
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Success 200 {object} controllers.HeroListResponse
// @Failure 500 {object} map[string]string
// @Router /api/v1/heroes [get]
func ListHeroes(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 > 100 {
perPage = 100
}
offset := (page - 1) * perPage
query := database.DB.Model(&models.Hero{}).Where("is_active = ?", true)
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var items []models.Hero
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})
}
// GetHero godoc
// @Summary Public: Get a hero by id
// @Description Return a single hero by id
// @Tags heroes
// @Produce json
// @Param id path int true "Hero ID"
// @Success 200 {object} controllers.HeroResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/heroes/{id} [get]
func GetHero(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 h models.Hero
if err := database.DB.First(&h, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "hero not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": h})
}

View File

@@ -0,0 +1,758 @@
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})
}

View File

@@ -0,0 +1,374 @@
package controllers
import (
"net/http"
"strconv"
"time"
database "goGin/app/database/config"
"goGin/app/database/models"
"goGin/app/middlewares"
utils "goGin/pkg/utis"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// UserResponse, kullanıcı verilerini güvenli bir şekilde döndürmek için
type UserResponse struct {
ID uint `json:"id"`
UserName string `json:"username"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
IsAdmin bool `json:"is_admin"`
}
// AdminUserListItem, admin listesinde deleted_at ile ayırt etmek için
type AdminUserListItem struct {
UserResponse
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}
// UserPayload, kullanıcı güncelleme payload'u
type UserPayload struct {
UserName string `json:"username"`
Email string `json:"email"`
Password string `json:"password,omitempty"` // Opsiyonel şifre güncellemesi
}
// AdminUserUpdatePayload, admin tarafından kullanıcı güncelleme
type AdminUserUpdatePayload struct {
UserName string `json:"username"`
Email string `json:"email"`
IsAdmin *bool `json:"is_admin"` // Pointer allows checking if field is present
}
// Helper to convert model to response
func toUserResponse(u models.User) UserResponse {
isAdmin := false
if u.IsAdmin != nil {
isAdmin = *u.IsAdmin
}
isVerified := false
if u.EmailVerified != nil {
isVerified = *u.EmailVerified
}
return UserResponse{
ID: u.ID,
UserName: u.UserName,
Email: u.Email,
EmailVerified: isVerified,
IsAdmin: isAdmin,
}
}
// toAdminUserListItem, admin listesinde deleted_at döndürmek için
func toAdminUserListItem(u models.User) AdminUserListItem {
item := AdminUserListItem{UserResponse: toUserResponse(u)}
if u.DeletedAt.Valid {
item.DeletedAt = &u.DeletedAt.Time
}
return item
}
// GetProfile godoc
// @Summary Get current user profile
// @Description Get profile of the logged-in user
// @Tags users
// @Security BearerAuth
// @Produce json
// @Success 200 {object} controllers.UserResponse
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/users/profile [get]
func GetProfile(c *gin.Context) {
claims, ok := middlewares.GetAuthClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var user models.User
if err := database.DB.Preload("SocialAccounts").Preload("Profile").First(&user, claims.UserID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": toUserResponse(user)})
}
// UpdateProfile godoc
// @Summary Update current user profile
// @Description Update profile of the logged-in user
// @Tags users
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param user body UserPayload true "User update payload"
// @Success 200 {object} controllers.UserResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/users/profile [put]
func UpdateProfile(c *gin.Context) {
claims, ok := middlewares.GetAuthClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var payload UserPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := database.DB.First(&user, claims.UserID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
if payload.UserName != "" {
user.UserName = payload.UserName
}
if payload.Email != "" {
user.Email = payload.Email
// Email değişirse doğrulama sıfırlanabilir
f := false
user.EmailVerified = &f
}
if payload.Password != "" {
hashed, err := utils.HashPassword(payload.Password)
if err == nil {
user.Password = hashed
}
}
if err := database.DB.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": toUserResponse(user)})
}
// AdminListUsers godoc
// @Summary Admin: List users
// @Description Admin listing of users with pagination and search
// @Tags users_admin
// @Security BearerAuth
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Param q query string false "Search query (username or email)"
// @Param soft query string false "Soft delete filter: only|with"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/users [get]
func AdminListUsers(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 > 100 {
perPage = 100
}
offset := (page - 1) * perPage
soft := c.Query("soft")
var query *gorm.DB
if soft == "only" {
query = database.DB.Unscoped().Model(&models.User{}).Where("deleted_at IS NOT NULL")
} else if soft == "with" {
query = database.DB.Unscoped().Model(&models.User{})
} else {
query = database.DB.Model(&models.User{})
}
if q := c.Query("q"); q != "" {
like := "%" + q + "%"
query = query.Where("user_name LIKE ? OR email LIKE ?", like, like)
}
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var users []models.User
if err := query.Order("created_at desc").Limit(perPage).Offset(offset).Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var data []AdminUserListItem
for _, u := range users {
data = append(data, toAdminUserListItem(u))
}
c.JSON(http.StatusOK, gin.H{"items": data, "total": total, "page": page, "per_page": perPage})
}
// AdminGetUser godoc
// @Summary Admin: Get user
// @Description Get user details by ID
// @Tags users_admin
// @Security BearerAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} controllers.UserResponse
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/users/{id} [get]
func AdminGetUser(c *gin.Context) {
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 user models.User
// Admin deleted kullanıcıyı da görebilmeli mi? Genelde evet, soft=with ile listede görüyorsa detayda da görmeli.
// Varsayılan olarak normal get soft-deleted getirmez. Unscoped kullanalım veya id ile direk bakalım.
if err := database.DB.Unscoped().First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": toUserResponse(user)})
}
// AdminUpdateUser godoc
// @Summary Admin: Update user
// @Description Update user details (admin)
// @Tags users_admin
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Param user body AdminUserUpdatePayload true "User update payload"
// @Success 200 {object} controllers.UserResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/users/{id} [put]
func AdminUpdateUser(c *gin.Context) {
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 payload AdminUserUpdatePayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := database.DB.Unscoped().First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
if payload.UserName != "" {
user.UserName = payload.UserName
}
if payload.Email != "" {
user.Email = payload.Email
}
if payload.IsAdmin != nil {
user.IsAdmin = payload.IsAdmin
}
if err := database.DB.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": toUserResponse(user)})
}
// AdminDeleteUser godoc
// @Summary Admin: Delete user
// @Description Soft delete user
// @Tags users_admin
// @Security BearerAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/users/{id} [delete]
func AdminDeleteUser(c *gin.Context) {
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 user models.User
if err := database.DB.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
if err := database.DB.Delete(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "user deleted successfully",
"id": user.ID,
})
}
// AdminRestoreUser godoc
// @Summary Admin: Restore user
// @Description Restore soft-deleted user
// @Tags users_admin
// @Security BearerAuth
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} controllers.UserResponse
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/users/{id}/restore [post]
func AdminRestoreUser(c *gin.Context) {
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 user models.User
if err := database.DB.Unscoped().First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
if user.DeletedAt.Valid {
// Restore
if err := database.DB.Unscoped().Model(&user).Update("deleted_at", nil).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"data": toUserResponse(user)})
}

View File

@@ -0,0 +1,137 @@
package controllers
import "time"
// Note: these structs are only used for Swagger documentation generation.
// They intentionally avoid embedding gorm.Model to keep swag parser happy.
type CategorySimple struct {
ID uint `json:"id" example:"1"`
Title string `json:"title" example:"News"`
Slug string `json:"slug" example:"news"`
ParentID *uint `json:"parent_id,omitempty"`
}
// AdminCategoryListItem admin listesinde deleted_at ile ayırt etmek için
type AdminCategoryListItem struct {
ID uint `json:"id" example:"1"`
Title string `json:"title" example:"News"`
Slug string `json:"slug" example:"news"`
ParentID *uint `json:"parent_id,omitempty"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}
type TagSimple struct {
ID uint `json:"id" example:"1"`
Name string `json:"name" example:"golang"`
}
// AdminTagListItem admin listesinde deleted_at ile ayırt etmek için
type AdminTagListItem struct {
ID uint `json:"id" example:"1"`
Name string `json:"name" example:"golang"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}
type PostResponse struct {
ID uint `json:"id" example:"1"`
Title string `json:"title" example:"My post title"`
Slug string `json:"slug" example:"my-post-title"`
Images string `json:"images"`
Content string `json:"content"`
Categories []CategorySimple `json:"categories,omitempty"`
Tags []TagSimple `json:"tags,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type PostListResponse struct {
Items []PostResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
}
// New swagger-only types
type CommentSimple struct {
ID uint `json:"id" example:"1"`
UserID uint `json:"user_id" example:"2"`
PostID uint `json:"post_id" example:"1"`
Body string `json:"body" example:"Nice post"`
Created time.Time `json:"created_at"`
}
type CategoryViewSimple struct {
ID uint `json:"id" example:"1"`
CategoryID uint `json:"category_id" example:"1"`
IPAddress string `json:"ip_address" example:"127.0.0.1"`
Created time.Time `json:"created_at"`
}
// Setting swagger-only types
type SettingResponse struct {
ID uint `json:"id" example:"1"`
Title string `json:"title" example:"Site Başlığı"`
MetaTitle string `json:"meta_title" example:"Meta Başlık"`
MetaDescription string `json:"meta_description" example:"Site açıklaması"`
Phone string `json:"phone" example:" +90 555 555 55 55"`
URL string `json:"url" example:"https://example.com"`
Email string `json:"email" example:"info@example.com"`
Facebook string `json:"facebook" example:"https://facebook.com/example"`
X string `json:"x" example:"https://x.com/example"`
Instagram string `json:"instagram" example:"https://instagram.com/example"`
Whatsapp string `json:"whatsapp" example:"https://wa.me/90555"`
Pinterest string `json:"pinterest" example:"https://pinterest.com/example"`
Linkedin string `json:"linkedin" example:"https://linkedin.com/company/example"`
Slogan string `json:"slogan" example:"En iyi içerik"`
Address string `json:"address" example:"Adres örneği"`
Copyright string `json:"copyright" example:"© 2026 Example"`
MapEmbed string `json:"map_embed"`
WLogo string `json:"w_logo"`
BLogo string `json:"b_logo"`
IsActive bool `json:"is_active"`
// image transform / metadata fields (match app/database/models/setting.go)
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"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type SettingListResponse struct {
Items []SettingResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
}
// Hero swagger-only types
type HeroResponse struct {
ID uint `json:"id" example:"1"`
Color string `json:"color" example:"#ffffff"`
Title string `json:"title" example:"Hero Başlık"`
Text1 string `json:"text1" example:"Kısa açıklama"`
Text2 string `json:"text2" example:"İkinci metin"`
Text4 string `json:"text4" example:"Yardımcı metin"`
Text5 string `json:"text5" example:"Ek metin"`
Image string `json:"image" example:"/uploads/heroes/img.jpg"`
IsActive bool `json:"is_active"`
Width int `json:"width" example:"1920"`
Height int `json:"height" example:"1080"`
Quality int `json:"quality" example:"80"`
Format string `json:"format" example:"jpeg"`
Created time.Time `json:"created_at"`
Updated time.Time `json:"updated_at"`
}
type HeroListResponse struct {
Items []HeroResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
}