first commit
This commit is contained in:
615
app/controllers/AuthControllers.go
Normal file
615
app/controllers/AuthControllers.go
Normal 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,
|
||||
})
|
||||
}
|
||||
2289
app/controllers/BlogContraller.go
Normal file
2289
app/controllers/BlogContraller.go
Normal file
File diff suppressed because it is too large
Load Diff
538
app/controllers/HeroController.go
Normal file
538
app/controllers/HeroController.go
Normal 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})
|
||||
}
|
||||
758
app/controllers/SettingController.go
Normal file
758
app/controllers/SettingController.go
Normal 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})
|
||||
}
|
||||
374
app/controllers/UserControllers.go
Normal file
374
app/controllers/UserControllers.go
Normal 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)})
|
||||
}
|
||||
137
app/controllers/swagger_models.go
Normal file
137
app/controllers/swagger_models.go
Normal 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user