first commit
This commit is contained in:
568
api/handlers/about_handler.go
Normal file
568
api/handlers/about_handler.go
Normal file
@@ -0,0 +1,568 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gauth-central/config"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AboutHandler struct {
|
||||
aboutService *services.AboutService
|
||||
}
|
||||
|
||||
func NewAboutHandler(aboutService *services.AboutService) *AboutHandler {
|
||||
return &AboutHandler{aboutService: aboutService}
|
||||
}
|
||||
|
||||
// GetAllAbout godoc
|
||||
// @Summary Get all active about entries
|
||||
// @Description Retrieve a list of active about entries
|
||||
// @Tags about
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.About
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /about [get]
|
||||
func (h *AboutHandler) GetAllAbout(c *gin.Context) {
|
||||
abouts, err := h.aboutService.GetAllAbout(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, abouts)
|
||||
}
|
||||
|
||||
// GetActiveAbout godoc
|
||||
// @Summary Get active about entry
|
||||
// @Description Retrieve the newest active about entry
|
||||
// @Tags about
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.About
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /about/active [get]
|
||||
func (h *AboutHandler) GetActiveAbout(c *gin.Context) {
|
||||
about, err := h.aboutService.GetFirstActiveAbout()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, about)
|
||||
}
|
||||
|
||||
// AdminGetAllAbout godoc
|
||||
// @Summary Get all about entries (Admin)
|
||||
// @Description Retrieve a list of all about entries including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.About
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/about [get]
|
||||
func (h *AboutHandler) AdminGetAllAbout(c *gin.Context) {
|
||||
abouts, err := h.aboutService.GetAllAbout(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, abouts)
|
||||
}
|
||||
|
||||
// AdminGetAboutByID godoc
|
||||
// @Summary Get an about entry by ID (Admin)
|
||||
// @Description Retrieve details of a specific about entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "About ID"
|
||||
// @Success 200 {object} models.About
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/about/{id} [get]
|
||||
func (h *AboutHandler) AdminGetAboutByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
about, err := h.aboutService.GetAboutByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, about)
|
||||
}
|
||||
|
||||
// CreateAbout godoc
|
||||
// @Summary Create a new about entry (Admin)
|
||||
// @Description Create a new about entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param title formData string true "Title"
|
||||
// @Param image formData file false "Image"
|
||||
// @Param image_sub formData string false "Image subtitle"
|
||||
// @Param cv formData file false "CV file"
|
||||
// @Param birthday formData string false "Birthday"
|
||||
// @Param city formData string false "City"
|
||||
// @Param study formData string false "Study"
|
||||
// @Param website formData string false "Website"
|
||||
// @Param phone formData string false "Phone"
|
||||
// @Param age formData string false "Age"
|
||||
// @Param interests formData string false "Interests"
|
||||
// @Param degree formData string false "Degree"
|
||||
// @Param x formData string false "X"
|
||||
// @Param mail formData string false "Mail"
|
||||
// @Param done formData int false "Done"
|
||||
// @Param project_done formData string false "Project done"
|
||||
// @Param user_h formData int false "User count"
|
||||
// @Param hapy_user formData string false "Happy user"
|
||||
// @Param great formData int false "Great"
|
||||
// @Param great_reviews formData string false "Great reviews"
|
||||
// @Param team formData int false "Team"
|
||||
// @Param support_team formData string false "Support team"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Param counter_active formData bool false "Counter active"
|
||||
// @Success 201 {object} models.About
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/about [post]
|
||||
func (h *AboutHandler) CreateAbout(c *gin.Context) {
|
||||
title := strings.TrimSpace(c.PostForm("title"))
|
||||
if title == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
|
||||
return
|
||||
}
|
||||
|
||||
imageSub := strings.TrimSpace(c.PostForm("image_sub"))
|
||||
birthday := strings.TrimSpace(c.PostForm("birthday"))
|
||||
city := strings.TrimSpace(c.PostForm("city"))
|
||||
study := strings.TrimSpace(c.PostForm("study"))
|
||||
website := strings.TrimSpace(c.PostForm("website"))
|
||||
phone := strings.TrimSpace(c.PostForm("phone"))
|
||||
age := strings.TrimSpace(c.PostForm("age"))
|
||||
interests := strings.TrimSpace(c.PostForm("interests"))
|
||||
degree := strings.TrimSpace(c.PostForm("degree"))
|
||||
xValue := strings.TrimSpace(c.PostForm("x"))
|
||||
mail := strings.TrimSpace(c.PostForm("mail"))
|
||||
projectDone := strings.TrimSpace(c.PostForm("project_done"))
|
||||
hapyUser := strings.TrimSpace(c.PostForm("hapy_user"))
|
||||
greatReviews := strings.TrimSpace(c.PostForm("great_reviews"))
|
||||
supportTeam := strings.TrimSpace(c.PostForm("support_team"))
|
||||
|
||||
done, err := parseOptionalInt(c, "done")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "done must be a number"})
|
||||
return
|
||||
}
|
||||
userH, err := parseOptionalInt(c, "user_h")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user_h must be a number"})
|
||||
return
|
||||
}
|
||||
great, err := parseOptionalInt(c, "great")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "great must be a number"})
|
||||
return
|
||||
}
|
||||
team, err := parseOptionalInt(c, "team")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "team must be a number"})
|
||||
return
|
||||
}
|
||||
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
counterActivePtr, err := parseOptionalBool(c, "counter_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "counter_active must be true or false"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := false
|
||||
if isActivePtr != nil {
|
||||
isActive = *isActivePtr
|
||||
}
|
||||
counterActive := false
|
||||
if counterActivePtr != nil {
|
||||
counterActive = *counterActivePtr
|
||||
}
|
||||
|
||||
imageURL := ""
|
||||
imageFile, imageErr := c.FormFile("image")
|
||||
if imageErr == nil {
|
||||
if imageFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
imageURL, err = utils.SaveOptimizedImage(imageFile, "./uploads/about", "about", &utils.ImageOptions{
|
||||
Width: config.AppConfig.AboutImageWidth,
|
||||
Height: config.AppConfig.AboutImageHeight,
|
||||
Quality: float32(config.AppConfig.AboutImageQuality),
|
||||
Format: config.AppConfig.AboutImageFormat,
|
||||
Mode: config.AppConfig.AboutImageMode,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cvPath := ""
|
||||
cvFile, cvErr := c.FormFile("cv")
|
||||
if cvErr == nil {
|
||||
cvPath, err = saveUploadedFile(c, cvFile, "./uploads/cv", "cv")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save CV: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
about, err := h.aboutService.CreateAbout(
|
||||
title,
|
||||
imageURL,
|
||||
imageSub,
|
||||
cvPath,
|
||||
birthday,
|
||||
city,
|
||||
study,
|
||||
website,
|
||||
phone,
|
||||
age,
|
||||
interests,
|
||||
degree,
|
||||
xValue,
|
||||
mail,
|
||||
done,
|
||||
projectDone,
|
||||
userH,
|
||||
hapyUser,
|
||||
great,
|
||||
greatReviews,
|
||||
team,
|
||||
supportTeam,
|
||||
isActive,
|
||||
counterActive,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, about)
|
||||
}
|
||||
|
||||
// UpdateAbout godoc
|
||||
// @Summary Update an about entry (Admin)
|
||||
// @Description Update an existing about entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "About ID"
|
||||
// @Param title formData string false "Title"
|
||||
// @Param image formData file false "Image"
|
||||
// @Param image_sub formData string false "Image subtitle"
|
||||
// @Param cv formData file false "CV file"
|
||||
// @Param birthday formData string false "Birthday"
|
||||
// @Param city formData string false "City"
|
||||
// @Param study formData string false "Study"
|
||||
// @Param website formData string false "Website"
|
||||
// @Param phone formData string false "Phone"
|
||||
// @Param age formData string false "Age"
|
||||
// @Param interests formData string false "Interests"
|
||||
// @Param degree formData string false "Degree"
|
||||
// @Param x formData string false "X"
|
||||
// @Param mail formData string false "Mail"
|
||||
// @Param done formData int false "Done"
|
||||
// @Param project_done formData string false "Project done"
|
||||
// @Param user_h formData int false "User count"
|
||||
// @Param hapy_user formData string false "Happy user"
|
||||
// @Param great formData int false "Great"
|
||||
// @Param great_reviews formData string false "Great reviews"
|
||||
// @Param team formData int false "Team"
|
||||
// @Param support_team formData string false "Support team"
|
||||
// @Param slug formData string false "Slug"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Param counter_active formData bool false "Counter active"
|
||||
// @Success 200 {object} models.About
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/about/{id} [put]
|
||||
func (h *AboutHandler) UpdateAbout(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
title, hasTitle := getOptionalFormValue(c, "title")
|
||||
imageSub, hasImageSub := getOptionalFormValue(c, "image_sub")
|
||||
birthday, hasBirthday := getOptionalFormValue(c, "birthday")
|
||||
city, hasCity := getOptionalFormValue(c, "city")
|
||||
study, hasStudy := getOptionalFormValue(c, "study")
|
||||
website, hasWebsite := getOptionalFormValue(c, "website")
|
||||
phone, hasPhone := getOptionalFormValue(c, "phone")
|
||||
age, hasAge := getOptionalFormValue(c, "age")
|
||||
interests, hasInterests := getOptionalFormValue(c, "interests")
|
||||
degree, hasDegree := getOptionalFormValue(c, "degree")
|
||||
xValue, hasX := getOptionalFormValue(c, "x")
|
||||
mail, hasMail := getOptionalFormValue(c, "mail")
|
||||
projectDone, hasProjectDone := getOptionalFormValue(c, "project_done")
|
||||
hapyUser, hasHapyUser := getOptionalFormValue(c, "hapy_user")
|
||||
greatReviews, hasGreatReviews := getOptionalFormValue(c, "great_reviews")
|
||||
supportTeam, hasSupportTeam := getOptionalFormValue(c, "support_team")
|
||||
slug, hasSlug := getOptionalFormValue(c, "slug")
|
||||
|
||||
done, err := parseOptionalInt(c, "done")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "done must be a number"})
|
||||
return
|
||||
}
|
||||
userH, err := parseOptionalInt(c, "user_h")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user_h must be a number"})
|
||||
return
|
||||
}
|
||||
great, err := parseOptionalInt(c, "great")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "great must be a number"})
|
||||
return
|
||||
}
|
||||
team, err := parseOptionalInt(c, "team")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "team must be a number"})
|
||||
return
|
||||
}
|
||||
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
counterActivePtr, err := parseOptionalBool(c, "counter_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "counter_active must be true or false"})
|
||||
return
|
||||
}
|
||||
|
||||
var titlePtr *string
|
||||
var imageSubPtr *string
|
||||
var birthdayPtr *string
|
||||
var cityPtr *string
|
||||
var studyPtr *string
|
||||
var websitePtr *string
|
||||
var phonePtr *string
|
||||
var agePtr *string
|
||||
var interestsPtr *string
|
||||
var degreePtr *string
|
||||
var xPtr *string
|
||||
var mailPtr *string
|
||||
var projectDonePtr *string
|
||||
var hapyUserPtr *string
|
||||
var greatReviewsPtr *string
|
||||
var supportTeamPtr *string
|
||||
var slugPtr *string
|
||||
|
||||
if hasTitle {
|
||||
titlePtr = &title
|
||||
}
|
||||
if hasImageSub {
|
||||
imageSubPtr = &imageSub
|
||||
}
|
||||
if hasBirthday {
|
||||
birthdayPtr = &birthday
|
||||
}
|
||||
if hasCity {
|
||||
cityPtr = &city
|
||||
}
|
||||
if hasStudy {
|
||||
studyPtr = &study
|
||||
}
|
||||
if hasWebsite {
|
||||
websitePtr = &website
|
||||
}
|
||||
if hasPhone {
|
||||
phonePtr = &phone
|
||||
}
|
||||
if hasAge {
|
||||
agePtr = &age
|
||||
}
|
||||
if hasInterests {
|
||||
interestsPtr = &interests
|
||||
}
|
||||
if hasDegree {
|
||||
degreePtr = °ree
|
||||
}
|
||||
if hasX {
|
||||
xPtr = &xValue
|
||||
}
|
||||
if hasMail {
|
||||
mailPtr = &mail
|
||||
}
|
||||
if hasProjectDone {
|
||||
projectDonePtr = &projectDone
|
||||
}
|
||||
if hasHapyUser {
|
||||
hapyUserPtr = &hapyUser
|
||||
}
|
||||
if hasGreatReviews {
|
||||
greatReviewsPtr = &greatReviews
|
||||
}
|
||||
if hasSupportTeam {
|
||||
supportTeamPtr = &supportTeam
|
||||
}
|
||||
if hasSlug {
|
||||
slugPtr = &slug
|
||||
}
|
||||
|
||||
var imagePtr *string
|
||||
imageFile, imageErr := c.FormFile("image")
|
||||
if imageErr == nil {
|
||||
if imageFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
about, fetchErr := h.aboutService.GetAboutByID(id)
|
||||
if fetchErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
||||
return
|
||||
}
|
||||
if about.Image != "" && strings.HasPrefix(about.Image, "/uploads/") {
|
||||
oldPath := "." + about.Image
|
||||
_ = os.Remove(oldPath)
|
||||
}
|
||||
imageURL, saveErr := utils.SaveOptimizedImage(imageFile, "./uploads/about", id, &utils.ImageOptions{
|
||||
Width: config.AppConfig.AboutImageWidth,
|
||||
Height: config.AppConfig.AboutImageHeight,
|
||||
Quality: float32(config.AppConfig.AboutImageQuality),
|
||||
Format: config.AppConfig.AboutImageFormat,
|
||||
Mode: config.AppConfig.AboutImageMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
imagePtr = &imageURL
|
||||
}
|
||||
|
||||
var cvPtr *string
|
||||
cvFile, cvErr := c.FormFile("cv")
|
||||
if cvErr == nil {
|
||||
about, fetchErr := h.aboutService.GetAboutByID(id)
|
||||
if fetchErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
||||
return
|
||||
}
|
||||
if about.CV != "" && strings.HasPrefix(about.CV, "/uploads/") {
|
||||
oldPath := "." + about.CV
|
||||
_ = os.Remove(oldPath)
|
||||
}
|
||||
cvPath, saveErr := saveUploadedFile(c, cvFile, "./uploads/cv", "cv")
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save CV: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
cvPtr = &cvPath
|
||||
}
|
||||
|
||||
about, err := h.aboutService.UpdateAbout(
|
||||
id,
|
||||
titlePtr,
|
||||
imagePtr,
|
||||
imageSubPtr,
|
||||
cvPtr,
|
||||
birthdayPtr,
|
||||
cityPtr,
|
||||
studyPtr,
|
||||
websitePtr,
|
||||
phonePtr,
|
||||
agePtr,
|
||||
interestsPtr,
|
||||
degreePtr,
|
||||
xPtr,
|
||||
mailPtr,
|
||||
done,
|
||||
projectDonePtr,
|
||||
userH,
|
||||
hapyUserPtr,
|
||||
great,
|
||||
greatReviewsPtr,
|
||||
team,
|
||||
supportTeamPtr,
|
||||
slugPtr,
|
||||
isActivePtr,
|
||||
counterActivePtr,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "about not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, about)
|
||||
}
|
||||
|
||||
// DeleteAbout godoc
|
||||
// @Summary Delete an about entry (Admin)
|
||||
// @Description Delete an about entry by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "About ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/about/{id} [delete]
|
||||
func (h *AboutHandler) DeleteAbout(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.aboutService.DeleteAbout(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "About deleted successfully"})
|
||||
}
|
||||
|
||||
func parseOptionalInt(c *gin.Context, key string) (*int, error) {
|
||||
value, exists := c.GetPostForm(key)
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
func saveUploadedFile(c *gin.Context, file *multipart.FileHeader, uploadDir string, prefix string) (string, error) {
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ext := filepath.Ext(file.Filename)
|
||||
filename := fmt.Sprintf("%s_%d%s", prefix, time.Now().UnixNano(), ext)
|
||||
fullPath := filepath.Join(uploadDir, filename)
|
||||
|
||||
if err := c.SaveUploadedFile(file, fullPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
relPath := filepath.Join(uploadDir, filename)
|
||||
if strings.HasPrefix(relPath, ".") {
|
||||
relPath = relPath[1:]
|
||||
}
|
||||
if !strings.HasPrefix(relPath, "/") {
|
||||
relPath = "/" + relPath
|
||||
}
|
||||
|
||||
return relPath, nil
|
||||
}
|
||||
275
api/handlers/auth_handler.go
Normal file
275
api/handlers/auth_handler.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gauth-central/config"
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/markbates/goth/gothic"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
|
||||
return &AuthHandler{authService: authService}
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
UserName string `json:"username" binding:"required,min=3"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type RefreshRequest struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
}
|
||||
|
||||
// Register godoc
|
||||
// @Summary Register a new user
|
||||
// @Description Register with username, email and password
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RegisterRequest true "Register Request"
|
||||
// @Success 201 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /auth/register [post]
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Register creates user with email_verified=false; tokens only after email verification
|
||||
user, _, _, verifyToken, err := h.authService.Register(req.UserName, req.Email, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Send verification email asynchronously
|
||||
go func() {
|
||||
if err := utils.SendVerificationEmail(user.Email, verifyToken); err != nil {
|
||||
fmt.Printf("Failed to send verification email to %s: %v\n", user.Email, err)
|
||||
} else {
|
||||
fmt.Printf("Verification email sent to %s\n", user.Email)
|
||||
}
|
||||
}()
|
||||
|
||||
roles := user.Roles
|
||||
if roles == nil {
|
||||
roles = []models.Role{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "User created. Please verify your email.",
|
||||
"user_id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"roles": roles,
|
||||
"email_verified": false,
|
||||
"verification_token": verifyToken, // Returned for dev convenience, usually hidden in prod
|
||||
})
|
||||
}
|
||||
|
||||
// Login godoc
|
||||
// @Summary Login user
|
||||
// @Description Login with email and password to get JWT token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body LoginRequest true "Login Request"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/login [post]
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, accessToken, refreshToken, err := h.authService.Login(req.Email, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure roles is always returned, even if empty
|
||||
roles := user.Roles
|
||||
if roles == nil {
|
||||
roles = []models.Role{}
|
||||
}
|
||||
|
||||
// Calculate expires timestamp (15 minutes from now, matching JWT expiry)
|
||||
expiresAt := time.Now().Add(15 * time.Minute)
|
||||
|
||||
// Match OAuth response format for frontend consistency
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"name": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
},
|
||||
"accessToken": accessToken,
|
||||
"refreshToken": refreshToken,
|
||||
"expires": expiresAt.Format(time.RFC3339),
|
||||
"roles": roles,
|
||||
})
|
||||
}
|
||||
|
||||
// BeginAuth godoc
|
||||
// @Summary Start OAuth2 flow
|
||||
// @Description Redirect to OAuth2 provider
|
||||
// @Tags oauth
|
||||
// @Param provider path string true "Provider (google, github)"
|
||||
// @Router /auth/{provider} [get]
|
||||
func (h *AuthHandler) BeginAuth(c *gin.Context) {
|
||||
// Try to complete user auth if we've already got a session
|
||||
// but context is not set correctly for gin with gothic usually
|
||||
provider := c.Param("provider")
|
||||
q := c.Request.URL.Query()
|
||||
q.Add("provider", provider)
|
||||
c.Request.URL.RawQuery = q.Encode()
|
||||
|
||||
gothic.BeginAuthHandler(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
// Callback godoc
|
||||
// @Summary OAuth2 Callback
|
||||
// @Description Handle callback from OAuth2 provider
|
||||
// @Tags oauth
|
||||
// @Param provider path string true "Provider (google, github)"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/{provider}/callback [get]
|
||||
func (h *AuthHandler) Callback(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
q := c.Request.URL.Query()
|
||||
q.Add("provider", provider)
|
||||
c.Request.URL.RawQuery = q.Encode()
|
||||
|
||||
gothUser, err := gothic.CompleteUserAuth(c.Writer, c.Request)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
_, accessToken, refreshToken, err := h.authService.FindOrCreateSocialUser(gothUser, provider)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Always redirect to frontend with tokens in URL fragment
|
||||
redirectBase := strings.TrimSpace(config.AppConfig.OAuthRedirectURL)
|
||||
if redirectBase == "" {
|
||||
// Default to localhost frontend if not configured
|
||||
redirectBase = "http://localhost:3000/auth/callback"
|
||||
}
|
||||
|
||||
// Construct redirect URL with tokens in fragment (hash) format
|
||||
redirectURL := fmt.Sprintf(
|
||||
"%s#access_token=%s&refresh_token=%s&provider=%s",
|
||||
strings.TrimRight(redirectBase, "#"),
|
||||
url.QueryEscape(accessToken),
|
||||
url.QueryEscape(refreshToken),
|
||||
url.QueryEscape(provider),
|
||||
)
|
||||
c.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
|
||||
// Refresh godoc
|
||||
// @Summary Refresh Access Token
|
||||
// @Description usage: send refresh_token to get new access_token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RefreshRequest true "Refresh Request"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *AuthHandler) Refresh(c *gin.Context) {
|
||||
var req RefreshRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, refreshToken, err := h.authService.RefreshToken(req.RefreshToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"access_token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyEmail godoc
|
||||
// @Summary Verify email address
|
||||
// @Description Verify email with token sent after email/password registration
|
||||
// @Tags auth
|
||||
// @Param token query string true "Verification token"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /auth/verify-email [get]
|
||||
func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"})
|
||||
return
|
||||
}
|
||||
if err := h.authService.VerifyEmail(token); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Email verified successfully"})
|
||||
}
|
||||
|
||||
// Me godoc
|
||||
// @Summary Get Current User Profile
|
||||
// @Description Get details of the currently authenticated user
|
||||
// @Tags auth
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/me [get]
|
||||
func (h *AuthHandler) Me(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.authService.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
267
api/handlers/auth_handler.go.bak
Normal file
267
api/handlers/auth_handler.go.bak
Normal file
@@ -0,0 +1,267 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gauth-central/config"
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/markbates/goth/gothic"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
|
||||
return &AuthHandler{authService: authService}
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
UserName string `json:"username" binding:"required,min=3"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type RefreshRequest struct {
|
||||
RefreshToken string `json:"refreshToken" binding:"required"`
|
||||
}
|
||||
|
||||
// Register godoc
|
||||
// @Summary Register a new user
|
||||
// @Description Register with username, email and password
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RegisterRequest true "Register Request"
|
||||
// @Success 201 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /auth/register [post]
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Register creates user with email_verified=false; tokens only after email verification
|
||||
user, _, _, verifyToken, err := h.authService.Register(req.UserName, req.Email, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Send verification email asynchronously
|
||||
go func() {
|
||||
if err := utils.SendVerificationEmail(user.Email, verifyToken); err != nil {
|
||||
fmt.Printf("Failed to send verification email to %s: %v\n", user.Email, err)
|
||||
} else {
|
||||
fmt.Printf("Verification email sent to %s\n", user.Email)
|
||||
}
|
||||
}()
|
||||
|
||||
roles := user.Roles
|
||||
if roles == nil {
|
||||
roles = []models.Role{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "User created. Please verify your email.",
|
||||
"user_id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"roles": roles,
|
||||
"email_verified": false,
|
||||
"verification_token": verifyToken, // Returned for dev convenience, usually hidden in prod
|
||||
})
|
||||
}
|
||||
|
||||
// Login godoc
|
||||
// @Summary Login user
|
||||
// @Description Login with email and password to get JWT token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body LoginRequest true "Login Request"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/login [post]
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, accessToken, refreshToken, err := h.authService.Login(req.Email, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure roles is always returned, even if empty
|
||||
roles := user.Roles
|
||||
if roles == nil {
|
||||
roles = []models.Role{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user_id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"roles": roles,
|
||||
"accessToken": accessToken,
|
||||
"refreshToken": refreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
// BeginAuth godoc
|
||||
// @Summary Start OAuth2 flow
|
||||
// @Description Redirect to OAuth2 provider
|
||||
// @Tags oauth
|
||||
// @Param provider path string true "Provider (google, github)"
|
||||
// @Router /auth/{provider} [get]
|
||||
func (h *AuthHandler) BeginAuth(c *gin.Context) {
|
||||
// Try to complete user auth if we've already got a session
|
||||
// but context is not set correctly for gin with gothic usually
|
||||
provider := c.Param("provider")
|
||||
q := c.Request.URL.Query()
|
||||
q.Add("provider", provider)
|
||||
c.Request.URL.RawQuery = q.Encode()
|
||||
|
||||
gothic.BeginAuthHandler(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
// Callback godoc
|
||||
// @Summary OAuth2 Callback
|
||||
// @Description Handle callback from OAuth2 provider
|
||||
// @Tags oauth
|
||||
// @Param provider path string true "Provider (google, github)"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/{provider}/callback [get]
|
||||
func (h *AuthHandler) Callback(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
q := c.Request.URL.Query()
|
||||
q.Add("provider", provider)
|
||||
c.Request.URL.RawQuery = q.Encode()
|
||||
|
||||
gothUser, err := gothic.CompleteUserAuth(c.Writer, c.Request)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
_, accessToken, refreshToken, err := h.authService.FindOrCreateSocialUser(gothUser, provider)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Always redirect to frontend with tokens in URL fragment
|
||||
redirectBase := strings.TrimSpace(config.AppConfig.OAuthRedirectURL)
|
||||
if redirectBase == "" {
|
||||
// Default to localhost frontend if not configured
|
||||
redirectBase = "http://localhost:3000/auth/callback"
|
||||
}
|
||||
|
||||
// Construct redirect URL with tokens in fragment (hash) format
|
||||
redirectURL := fmt.Sprintf(
|
||||
"%s#accessToken=%s&refreshToken=%s&provider=%s",
|
||||
strings.TrimRight(redirectBase, "#"),
|
||||
url.QueryEscape(accessToken),
|
||||
url.QueryEscape(refreshToken),
|
||||
url.QueryEscape(provider),
|
||||
)
|
||||
c.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
|
||||
// Refresh godoc
|
||||
// @Summary Refresh Access Token
|
||||
// @Description usage: send refreshToken to get new accessToken
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RefreshRequest true "Refresh Request"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *AuthHandler) Refresh(c *gin.Context) {
|
||||
var req RefreshRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, refreshToken, err := h.authService.RefreshToken(req.RefreshToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"accessToken": accessToken,
|
||||
"refreshToken": refreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyEmail godoc
|
||||
// @Summary Verify email address
|
||||
// @Description Verify email with token sent after email/password registration
|
||||
// @Tags auth
|
||||
// @Param token query string true "Verification token"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /auth/verify-email [get]
|
||||
func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"})
|
||||
return
|
||||
}
|
||||
if err := h.authService.VerifyEmail(token); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Email verified successfully"})
|
||||
}
|
||||
|
||||
// Me godoc
|
||||
// @Summary Get Current User Profile
|
||||
// @Description Get details of the currently authenticated user
|
||||
// @Tags auth
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/me [get]
|
||||
func (h *AuthHandler) Me(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.authService.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
193
api/handlers/avatar_handler.go
Normal file
193
api/handlers/avatar_handler.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/database"
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AvatarHandler struct{}
|
||||
|
||||
func NewAvatarHandler() *AvatarHandler {
|
||||
return &AvatarHandler{}
|
||||
}
|
||||
|
||||
// UploadAvatar godoc
|
||||
// @Summary Upload user avatar
|
||||
// @Tags User
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param avatar formData file true "Avatar image file"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /user/avatar [post]
|
||||
func (h *AvatarHandler) UploadAvatar(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse multipart form
|
||||
file, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user to check for old avatar
|
||||
var user models.User
|
||||
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete old avatar file if exists and is not from OAuth
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") {
|
||||
oldPath := "." + user.Avatar
|
||||
os.Remove(oldPath) // Ignore error if file doesn't exist
|
||||
}
|
||||
|
||||
// Use utils.SaveOptimizedImage
|
||||
avatarURL, err := utils.SaveOptimizedImage(file, "./uploads/avatars", userID, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update avatar URL
|
||||
if err := database.DB.Model(&user).Update("avatar", avatarURL).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update avatar"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Avatar uploaded successfully",
|
||||
"avatar_url": avatarURL,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": avatarURL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAvatar godoc
|
||||
// @Summary Delete user avatar
|
||||
// @Tags User
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /user/avatar [delete]
|
||||
func (h *AvatarHandler) DeleteAvatar(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete avatar file if it's a local upload
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") {
|
||||
filepath := "." + user.Avatar
|
||||
os.Remove(filepath) // Ignore error if file doesn't exist
|
||||
}
|
||||
|
||||
// Set avatar to empty string
|
||||
if err := database.DB.Model(&user).Update("avatar", "").Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete avatar"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Avatar deleted successfully",
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// AdminUploadAvatar godoc
|
||||
// @Summary Upload avatar for any user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Param avatar formData file true "Avatar image file"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id}/avatar [post]
|
||||
func (h *AvatarHandler) AdminUploadAvatar(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
// Parse multipart form
|
||||
file, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user to check for old avatar
|
||||
var user models.User
|
||||
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete old avatar file if exists and is not from OAuth
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") {
|
||||
oldPath := "." + user.Avatar
|
||||
os.Remove(oldPath) // Ignore error if file doesn't exist
|
||||
}
|
||||
|
||||
// Use utils.SaveOptimizedImage
|
||||
avatarURL, err := utils.SaveOptimizedImage(file, "./uploads/avatars", userID, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update avatar URL
|
||||
if err := database.DB.Model(&user).Update("avatar", avatarURL).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update avatar"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Avatar uploaded successfully",
|
||||
"avatar_url": avatarURL,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": avatarURL,
|
||||
},
|
||||
})
|
||||
}
|
||||
364
api/handlers/banner_handler.go
Normal file
364
api/handlers/banner_handler.go
Normal file
@@ -0,0 +1,364 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gauth-central/config"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type BannerHandler struct {
|
||||
bannerService *services.BannerService
|
||||
}
|
||||
|
||||
func NewBannerHandler(bannerService *services.BannerService) *BannerHandler {
|
||||
return &BannerHandler{bannerService: bannerService}
|
||||
}
|
||||
|
||||
// GetAllBanners godoc
|
||||
// @Summary Get all active banners
|
||||
// @Description Retrieve a list of active banners
|
||||
// @Tags banners
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Banner
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /banners [get]
|
||||
func (h *BannerHandler) GetAllBanners(c *gin.Context) {
|
||||
items, err := h.bannerService.GetAllBanners(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// GetActiveBanner godoc
|
||||
// @Summary Get active banner
|
||||
// @Description Retrieve the newest active banner
|
||||
// @Tags banners
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Banner
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /banners/active [get]
|
||||
func (h *BannerHandler) GetActiveBanner(c *gin.Context) {
|
||||
item, err := h.bannerService.GetFirstActiveBanner()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminGetAllBanners godoc
|
||||
// @Summary Get all banners (Admin)
|
||||
// @Description Retrieve a list of all banners including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Banner
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/banners [get]
|
||||
func (h *BannerHandler) AdminGetAllBanners(c *gin.Context) {
|
||||
items, err := h.bannerService.GetAllBanners(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetBannerByID godoc
|
||||
// @Summary Get banner by ID (Admin)
|
||||
// @Description Retrieve details of a specific banner
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Banner ID"
|
||||
// @Success 200 {object} models.Banner
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/banners/{id} [get]
|
||||
func (h *BannerHandler) AdminGetBannerByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.bannerService.GetBannerByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateBanner godoc
|
||||
// @Summary Create a new banner (Admin)
|
||||
// @Description Create a new banner entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param color formData string false "Text color"
|
||||
// @Param title formData string false "Title"
|
||||
// @Param text1 formData string false "Text 1"
|
||||
// @Param text2 formData string false "Text 2"
|
||||
// @Param text4 formData string false "Text 4"
|
||||
// @Param text5 formData string false "Text 5"
|
||||
// @Param image formData file true "Image"
|
||||
// @Param image_k formData file false "Small image"
|
||||
// @Param image_k_txt formData string false "Small image text"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Success 201 {object} models.Banner
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/banners [post]
|
||||
func (h *BannerHandler) CreateBanner(c *gin.Context) {
|
||||
color := strings.TrimSpace(c.PostForm("color"))
|
||||
title := strings.TrimSpace(c.PostForm("title"))
|
||||
text1 := strings.TrimSpace(c.PostForm("text1"))
|
||||
text2 := strings.TrimSpace(c.PostForm("text2"))
|
||||
text4 := strings.TrimSpace(c.PostForm("text4"))
|
||||
text5 := strings.TrimSpace(c.PostForm("text5"))
|
||||
imageKTxt := strings.TrimSpace(c.PostForm("image_k_txt"))
|
||||
|
||||
isActive := true
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
if isActivePtr != nil {
|
||||
isActive = *isActivePtr
|
||||
}
|
||||
|
||||
imageFile, imageErr := c.FormFile("image")
|
||||
if imageErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "image is required"})
|
||||
return
|
||||
}
|
||||
if imageFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
imageURL, err := utils.SaveOptimizedImage(imageFile, "./uploads/banner", "banner", &utils.ImageOptions{
|
||||
Width: config.AppConfig.BannerImageWidth,
|
||||
Height: config.AppConfig.BannerImageHeight,
|
||||
Quality: float32(config.AppConfig.BannerImageQuality),
|
||||
Format: config.AppConfig.BannerImageFormat,
|
||||
Mode: config.AppConfig.BannerImageMode,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
imageKURL := ""
|
||||
imageKFile, imageKErr := c.FormFile("image_k")
|
||||
if imageKErr == nil {
|
||||
if imageKFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "image_k size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
imageKURL, err = utils.SaveOptimizedImage(imageKFile, "./uploads/banner/kucuk", "banner_k", &utils.ImageOptions{
|
||||
Width: config.AppConfig.BannerThumbWidth,
|
||||
Height: config.AppConfig.BannerThumbHeight,
|
||||
Quality: float32(config.AppConfig.BannerThumbQuality),
|
||||
Format: config.AppConfig.BannerThumbFormat,
|
||||
Mode: config.AppConfig.BannerThumbMode,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image_k: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
item, err := h.bannerService.CreateBanner(
|
||||
color,
|
||||
title,
|
||||
text1,
|
||||
text2,
|
||||
text4,
|
||||
text5,
|
||||
imageURL,
|
||||
imageKURL,
|
||||
imageKTxt,
|
||||
isActive,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateBanner godoc
|
||||
// @Summary Update a banner (Admin)
|
||||
// @Description Update an existing banner entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "Banner ID"
|
||||
// @Param color formData string false "Text color"
|
||||
// @Param title formData string false "Title"
|
||||
// @Param text1 formData string false "Text 1"
|
||||
// @Param text2 formData string false "Text 2"
|
||||
// @Param text4 formData string false "Text 4"
|
||||
// @Param text5 formData string false "Text 5"
|
||||
// @Param image formData file false "Image"
|
||||
// @Param image_k formData file false "Small image"
|
||||
// @Param image_k_txt formData string false "Small image text"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Success 200 {object} models.Banner
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/banners/{id} [put]
|
||||
func (h *BannerHandler) UpdateBanner(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
color, hasColor := getOptionalFormValue(c, "color")
|
||||
title, hasTitle := getOptionalFormValue(c, "title")
|
||||
text1, hasText1 := getOptionalFormValue(c, "text1")
|
||||
text2, hasText2 := getOptionalFormValue(c, "text2")
|
||||
text4, hasText4 := getOptionalFormValue(c, "text4")
|
||||
text5, hasText5 := getOptionalFormValue(c, "text5")
|
||||
imageKTxt, hasImageKTxt := getOptionalFormValue(c, "image_k_txt")
|
||||
|
||||
var colorPtr *string
|
||||
var titlePtr *string
|
||||
var text1Ptr *string
|
||||
var text2Ptr *string
|
||||
var text4Ptr *string
|
||||
var text5Ptr *string
|
||||
var imageKTxtPtr *string
|
||||
|
||||
if hasColor {
|
||||
colorPtr = &color
|
||||
}
|
||||
if hasTitle {
|
||||
titlePtr = &title
|
||||
}
|
||||
if hasText1 {
|
||||
text1Ptr = &text1
|
||||
}
|
||||
if hasText2 {
|
||||
text2Ptr = &text2
|
||||
}
|
||||
if hasText4 {
|
||||
text4Ptr = &text4
|
||||
}
|
||||
if hasText5 {
|
||||
text5Ptr = &text5
|
||||
}
|
||||
if hasImageKTxt {
|
||||
imageKTxtPtr = &imageKTxt
|
||||
}
|
||||
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
|
||||
var imagePtr *string
|
||||
imageFile, imageErr := c.FormFile("image")
|
||||
if imageErr == nil {
|
||||
if imageFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
item, fetchErr := h.bannerService.GetBannerByID(id)
|
||||
if fetchErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
||||
return
|
||||
}
|
||||
if item.Image != "" && strings.HasPrefix(item.Image, "/uploads/") {
|
||||
_ = os.Remove("." + item.Image)
|
||||
}
|
||||
imageURL, saveErr := utils.SaveOptimizedImage(imageFile, "./uploads/banner", id, &utils.ImageOptions{
|
||||
Width: config.AppConfig.BannerImageWidth,
|
||||
Height: config.AppConfig.BannerImageHeight,
|
||||
Quality: float32(config.AppConfig.BannerImageQuality),
|
||||
Format: config.AppConfig.BannerImageFormat,
|
||||
Mode: config.AppConfig.BannerImageMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
imagePtr = &imageURL
|
||||
}
|
||||
|
||||
var imageKPtr *string
|
||||
imageKFile, imageKErr := c.FormFile("image_k")
|
||||
if imageKErr == nil {
|
||||
if imageKFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "image_k size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
item, fetchErr := h.bannerService.GetBannerByID(id)
|
||||
if fetchErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
||||
return
|
||||
}
|
||||
if item.ImageK != "" && strings.HasPrefix(item.ImageK, "/uploads/") {
|
||||
_ = os.Remove("." + item.ImageK)
|
||||
}
|
||||
imageKURL, saveErr := utils.SaveOptimizedImage(imageKFile, "./uploads/banner/kucuk", id+"_k", &utils.ImageOptions{
|
||||
Width: config.AppConfig.BannerThumbWidth,
|
||||
Height: config.AppConfig.BannerThumbHeight,
|
||||
Quality: float32(config.AppConfig.BannerThumbQuality),
|
||||
Format: config.AppConfig.BannerThumbFormat,
|
||||
Mode: config.AppConfig.BannerThumbMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image_k: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
imageKPtr = &imageKURL
|
||||
}
|
||||
|
||||
item, err := h.bannerService.UpdateBanner(
|
||||
id,
|
||||
colorPtr,
|
||||
titlePtr,
|
||||
text1Ptr,
|
||||
text2Ptr,
|
||||
text4Ptr,
|
||||
text5Ptr,
|
||||
imagePtr,
|
||||
imageKPtr,
|
||||
imageKTxtPtr,
|
||||
isActivePtr,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "banner not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteBanner godoc
|
||||
// @Summary Delete a banner (Admin)
|
||||
// @Description Delete a banner by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Banner ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/banners/{id} [delete]
|
||||
func (h *BannerHandler) DeleteBanner(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.bannerService.DeleteBanner(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Banner deleted successfully"})
|
||||
}
|
||||
128
api/handlers/contact_handler.go
Normal file
128
api/handlers/contact_handler.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ContactHandler struct {
|
||||
contactService *services.ContactService
|
||||
}
|
||||
|
||||
func NewContactHandler(contactService *services.ContactService) *ContactHandler {
|
||||
return &ContactHandler{contactService: contactService}
|
||||
}
|
||||
|
||||
type CreateContactRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Subject string `json:"subject" binding:"required"`
|
||||
Message string `json:"message" binding:"required"`
|
||||
}
|
||||
|
||||
// CreateContact godoc
|
||||
// @Summary Create a new contact message
|
||||
// @Description Send a contact message
|
||||
// @Tags contact
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreateContactRequest true "Contact Request"
|
||||
// @Success 201 {object} models.Contact
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /contact [post]
|
||||
func (h *ContactHandler) CreateContact(c *gin.Context) {
|
||||
var req CreateContactRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get IP address
|
||||
ip := c.ClientIP()
|
||||
|
||||
// Get User ID if authenticated (optional)
|
||||
var userID *string
|
||||
if id, exists := c.Get("user_id"); exists {
|
||||
idStr := id.(string)
|
||||
userID = &idStr
|
||||
}
|
||||
|
||||
contact, err := h.contactService.CreateContact(req.Name, req.Email, req.Subject, req.Message, ip, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, contact)
|
||||
}
|
||||
|
||||
// GetAllContacts godoc
|
||||
// @Summary Get all contact messages (Admin)
|
||||
// @Description Retrieve a list of all contact messages with pagination
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number"
|
||||
// @Param limit query int false "Items per page"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/contacts [get]
|
||||
func (h *ContactHandler) GetAllContacts(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
contacts, total, err := h.contactService.GetAllContacts(page, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": contacts,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
// GetContactByID godoc
|
||||
// @Summary Get a contact message by ID (Admin)
|
||||
// @Description Retrieve details of a specific contact message
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Contact ID"
|
||||
// @Success 200 {object} models.Contact
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/contacts/{id} [get]
|
||||
func (h *ContactHandler) GetContactByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
contact, err := h.contactService.GetContactByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, contact)
|
||||
}
|
||||
|
||||
// DeleteContact godoc
|
||||
// @Summary Delete a contact message (Admin)
|
||||
// @Description Delete a contact message by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Contact ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/contacts/{id} [delete]
|
||||
func (h *ContactHandler) DeleteContact(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.contactService.DeleteContact(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Contact deleted successfully"})
|
||||
}
|
||||
229
api/handlers/education_handler.go
Normal file
229
api/handlers/education_handler.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type EducationHandler struct {
|
||||
educationService *services.EducationService
|
||||
}
|
||||
|
||||
func NewEducationHandler(educationService *services.EducationService) *EducationHandler {
|
||||
return &EducationHandler{educationService: educationService}
|
||||
}
|
||||
|
||||
type CreateEducationRequest struct {
|
||||
BetweenYears string `json:"between_years"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
ResumeID string `json:"resume_id"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UpdateEducationRequest struct {
|
||||
BetweenYears *string `json:"between_years"`
|
||||
Title *string `json:"title"`
|
||||
Content *string `json:"content"`
|
||||
ResumeID *string `json:"resume_id"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// GetAllEducations godoc
|
||||
// @Summary Get all active educations
|
||||
// @Description Retrieve a list of active education entries
|
||||
// @Tags resume
|
||||
// @Produce json
|
||||
// @Param resume_id query string false "Resume ID"
|
||||
// @Success 200 {array} models.Education
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /educations [get]
|
||||
func (h *EducationHandler) GetAllEducations(c *gin.Context) {
|
||||
resumeID := strings.TrimSpace(c.Query("resume_id"))
|
||||
var resumeIDPtr *string
|
||||
if resumeID != "" {
|
||||
resumeIDPtr = &resumeID
|
||||
}
|
||||
items, err := h.educationService.GetAllEducations(resumeIDPtr, true)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetAllEducations godoc
|
||||
// @Summary Get all educations (Admin)
|
||||
// @Description Retrieve a list of all education entries including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param resume_id query string false "Resume ID"
|
||||
// @Success 200 {array} models.Education
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/educations [get]
|
||||
func (h *EducationHandler) AdminGetAllEducations(c *gin.Context) {
|
||||
resumeID := strings.TrimSpace(c.Query("resume_id"))
|
||||
var resumeIDPtr *string
|
||||
if resumeID != "" {
|
||||
resumeIDPtr = &resumeID
|
||||
}
|
||||
items, err := h.educationService.GetAllEducations(resumeIDPtr, false)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetEducationByID godoc
|
||||
// @Summary Get an education by ID (Admin)
|
||||
// @Description Retrieve details of a specific education entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Education ID"
|
||||
// @Success 200 {object} models.Education
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/educations/{id} [get]
|
||||
func (h *EducationHandler) AdminGetEducationByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.educationService.GetEducationByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateEducation godoc
|
||||
// @Summary Create a new education (Admin)
|
||||
// @Description Create a new education entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreateEducationRequest true "Education Request"
|
||||
// @Success 201 {object} models.Education
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/educations [post]
|
||||
func (h *EducationHandler) CreateEducation(c *gin.Context) {
|
||||
var req CreateEducationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
req.BetweenYears = strings.TrimSpace(req.BetweenYears)
|
||||
req.Title = strings.TrimSpace(req.Title)
|
||||
req.Content = strings.TrimSpace(req.Content)
|
||||
if req.BetweenYears == "" || req.Title == "" || req.Content == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "between_years, title, and content are required"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := false
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
resumeUUID, parseErr := parseUUIDPtr(req.ResumeID)
|
||||
if parseErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume id"})
|
||||
return
|
||||
}
|
||||
item, err := h.educationService.CreateEducation(req.BetweenYears, req.Title, req.Content, resumeUUID, isActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateEducation godoc
|
||||
// @Summary Update an education (Admin)
|
||||
// @Description Update an existing education entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Education ID"
|
||||
// @Param request body UpdateEducationRequest true "Education Request"
|
||||
// @Success 200 {object} models.Education
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/educations/{id} [put]
|
||||
func (h *EducationHandler) UpdateEducation(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req UpdateEducationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.BetweenYears != nil {
|
||||
trimmed := strings.TrimSpace(*req.BetweenYears)
|
||||
req.BetweenYears = &trimmed
|
||||
}
|
||||
if req.Title != nil {
|
||||
trimmed := strings.TrimSpace(*req.Title)
|
||||
req.Title = &trimmed
|
||||
}
|
||||
if req.Content != nil {
|
||||
trimmed := strings.TrimSpace(*req.Content)
|
||||
req.Content = &trimmed
|
||||
}
|
||||
if req.ResumeID != nil {
|
||||
trimmed := strings.TrimSpace(*req.ResumeID)
|
||||
req.ResumeID = &trimmed
|
||||
}
|
||||
|
||||
var resumeUUIDPtr *uuid.UUID
|
||||
if req.ResumeID != nil {
|
||||
parsed, parseErr := parseUUIDPtr(*req.ResumeID)
|
||||
if parseErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume id"})
|
||||
return
|
||||
}
|
||||
resumeUUIDPtr = parsed
|
||||
}
|
||||
item, err := h.educationService.UpdateEducation(id, req.BetweenYears, req.Title, req.Content, resumeUUIDPtr, req.IsActive)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "education not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteEducation godoc
|
||||
// @Summary Delete an education (Admin)
|
||||
// @Description Delete an education by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Education ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/educations/{id} [delete]
|
||||
func (h *EducationHandler) DeleteEducation(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.educationService.DeleteEducation(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Education deleted successfully"})
|
||||
}
|
||||
229
api/handlers/experience_handler.go
Normal file
229
api/handlers/experience_handler.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ExperienceHandler struct {
|
||||
experienceService *services.ExperienceService
|
||||
}
|
||||
|
||||
func NewExperienceHandler(experienceService *services.ExperienceService) *ExperienceHandler {
|
||||
return &ExperienceHandler{experienceService: experienceService}
|
||||
}
|
||||
|
||||
type CreateExperienceRequest struct {
|
||||
BetweenYears string `json:"between_years"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
ResumeID string `json:"resume_id"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UpdateExperienceRequest struct {
|
||||
BetweenYears *string `json:"between_years"`
|
||||
Title *string `json:"title"`
|
||||
Content *string `json:"content"`
|
||||
ResumeID *string `json:"resume_id"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// GetAllExperiences godoc
|
||||
// @Summary Get all active experiences
|
||||
// @Description Retrieve a list of active experience entries
|
||||
// @Tags resume
|
||||
// @Produce json
|
||||
// @Param resume_id query string false "Resume ID"
|
||||
// @Success 200 {array} models.Experience
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /experiences [get]
|
||||
func (h *ExperienceHandler) GetAllExperiences(c *gin.Context) {
|
||||
resumeID := strings.TrimSpace(c.Query("resume_id"))
|
||||
var resumeIDPtr *string
|
||||
if resumeID != "" {
|
||||
resumeIDPtr = &resumeID
|
||||
}
|
||||
items, err := h.experienceService.GetAllExperiences(resumeIDPtr, true)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetAllExperiences godoc
|
||||
// @Summary Get all experiences (Admin)
|
||||
// @Description Retrieve a list of all experience entries including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param resume_id query string false "Resume ID"
|
||||
// @Success 200 {array} models.Experience
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/experiences [get]
|
||||
func (h *ExperienceHandler) AdminGetAllExperiences(c *gin.Context) {
|
||||
resumeID := strings.TrimSpace(c.Query("resume_id"))
|
||||
var resumeIDPtr *string
|
||||
if resumeID != "" {
|
||||
resumeIDPtr = &resumeID
|
||||
}
|
||||
items, err := h.experienceService.GetAllExperiences(resumeIDPtr, false)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetExperienceByID godoc
|
||||
// @Summary Get an experience by ID (Admin)
|
||||
// @Description Retrieve details of a specific experience entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Experience ID"
|
||||
// @Success 200 {object} models.Experience
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/experiences/{id} [get]
|
||||
func (h *ExperienceHandler) AdminGetExperienceByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.experienceService.GetExperienceByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateExperience godoc
|
||||
// @Summary Create a new experience (Admin)
|
||||
// @Description Create a new experience entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreateExperienceRequest true "Experience Request"
|
||||
// @Success 201 {object} models.Experience
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/experiences [post]
|
||||
func (h *ExperienceHandler) CreateExperience(c *gin.Context) {
|
||||
var req CreateExperienceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
req.BetweenYears = strings.TrimSpace(req.BetweenYears)
|
||||
req.Title = strings.TrimSpace(req.Title)
|
||||
req.Content = strings.TrimSpace(req.Content)
|
||||
if req.BetweenYears == "" || req.Title == "" || req.Content == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "between_years, title, and content are required"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := false
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
resumeUUID, parseErr := parseUUIDPtr(req.ResumeID)
|
||||
if parseErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume id"})
|
||||
return
|
||||
}
|
||||
item, err := h.experienceService.CreateExperience(req.BetweenYears, req.Title, req.Content, resumeUUID, isActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateExperience godoc
|
||||
// @Summary Update an experience (Admin)
|
||||
// @Description Update an existing experience entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Experience ID"
|
||||
// @Param request body UpdateExperienceRequest true "Experience Request"
|
||||
// @Success 200 {object} models.Experience
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/experiences/{id} [put]
|
||||
func (h *ExperienceHandler) UpdateExperience(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req UpdateExperienceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.BetweenYears != nil {
|
||||
trimmed := strings.TrimSpace(*req.BetweenYears)
|
||||
req.BetweenYears = &trimmed
|
||||
}
|
||||
if req.Title != nil {
|
||||
trimmed := strings.TrimSpace(*req.Title)
|
||||
req.Title = &trimmed
|
||||
}
|
||||
if req.Content != nil {
|
||||
trimmed := strings.TrimSpace(*req.Content)
|
||||
req.Content = &trimmed
|
||||
}
|
||||
if req.ResumeID != nil {
|
||||
trimmed := strings.TrimSpace(*req.ResumeID)
|
||||
req.ResumeID = &trimmed
|
||||
}
|
||||
|
||||
var resumeUUIDPtr *uuid.UUID
|
||||
if req.ResumeID != nil {
|
||||
parsed, parseErr := parseUUIDPtr(*req.ResumeID)
|
||||
if parseErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume id"})
|
||||
return
|
||||
}
|
||||
resumeUUIDPtr = parsed
|
||||
}
|
||||
item, err := h.experienceService.UpdateExperience(id, req.BetweenYears, req.Title, req.Content, resumeUUIDPtr, req.IsActive)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "experience not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteExperience godoc
|
||||
// @Summary Delete an experience (Admin)
|
||||
// @Description Delete an experience by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Experience ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/experiences/{id} [delete]
|
||||
func (h *ExperienceHandler) DeleteExperience(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.experienceService.DeleteExperience(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Experience deleted successfully"})
|
||||
}
|
||||
46
api/handlers/form_helpers.go
Normal file
46
api/handlers/form_helpers.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func getOptionalFormValue(c *gin.Context, key string) (string, bool) {
|
||||
value, exists := c.GetPostForm(key)
|
||||
if !exists {
|
||||
return "", false
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
return value, true
|
||||
}
|
||||
|
||||
func parseOptionalBool(c *gin.Context, key string) (*bool, error) {
|
||||
value, exists := c.GetPostForm(key)
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
func parseUUIDPtr(value string) (*uuid.UUID, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := uuid.Parse(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
500
api/handlers/home_handler.go
Normal file
500
api/handlers/home_handler.go
Normal file
@@ -0,0 +1,500 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gauth-central/config"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type HomeHandler struct {
|
||||
homeService *services.HomeService
|
||||
}
|
||||
|
||||
func NewHomeHandler(homeService *services.HomeService) *HomeHandler {
|
||||
return &HomeHandler{homeService: homeService}
|
||||
}
|
||||
|
||||
type CreateHomeRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Button1 string `json:"button1" binding:"required"`
|
||||
Button2 string `json:"button2" binding:"required"`
|
||||
Video string `json:"video"`
|
||||
Keywords string `json:"keywords" binding:"required"`
|
||||
TagIDs []string `json:"tag_ids"`
|
||||
Image string `json:"image"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UpdateHomeRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Title *string `json:"title"`
|
||||
Button1 *string `json:"button1"`
|
||||
Button2 *string `json:"button2"`
|
||||
Video *string `json:"video"`
|
||||
Keywords *string `json:"keywords"`
|
||||
TagIDs *[]string `json:"tag_ids"`
|
||||
Image *string `json:"image"`
|
||||
Slug *string `json:"slug"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// GetAllHomes godoc
|
||||
// @Summary Get all active homes
|
||||
// @Description Retrieve a list of active home entries
|
||||
// @Tags home
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Home
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /homes [get]
|
||||
func (h *HomeHandler) GetAllHomes(c *gin.Context) {
|
||||
homes, err := h.homeService.GetAllHomes(true)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if isHomeBadRequest(err) {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, homes)
|
||||
}
|
||||
|
||||
// GetHomeBySlug godoc
|
||||
// @Summary Get a home by slug
|
||||
// @Description Retrieve a single active home by slug
|
||||
// @Tags home
|
||||
// @Produce json
|
||||
// @Param slug path string true "Home Slug"
|
||||
// @Success 200 {object} models.Home
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /homes/{slug} [get]
|
||||
func (h *HomeHandler) GetHomeBySlug(c *gin.Context) {
|
||||
slug := c.Param("slug")
|
||||
home, err := h.homeService.GetHomeBySlug(slug, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, home)
|
||||
}
|
||||
|
||||
// AdminGetAllHomes godoc
|
||||
// @Summary Get all homes (Admin)
|
||||
// @Description Retrieve a list of all homes including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Home
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/homes [get]
|
||||
func (h *HomeHandler) AdminGetAllHomes(c *gin.Context) {
|
||||
homes, err := h.homeService.GetAllHomes(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, homes)
|
||||
}
|
||||
|
||||
// AdminGetHomeByID godoc
|
||||
// @Summary Get a home by ID (Admin)
|
||||
// @Description Retrieve details of a specific home
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Home ID"
|
||||
// @Success 200 {object} models.Home
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/homes/{id} [get]
|
||||
func (h *HomeHandler) AdminGetHomeByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
home, err := h.homeService.GetHomeByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, home)
|
||||
}
|
||||
|
||||
// CreateHome godoc
|
||||
// @Summary Create a new home (Admin)
|
||||
// @Description Create a new home entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param name formData string true "Name"
|
||||
// @Param title formData string true "Title"
|
||||
// @Param button1 formData string true "Button 1"
|
||||
// @Param button2 formData string true "Button 2"
|
||||
// @Param video formData string false "Video URL"
|
||||
// @Param keywords formData string true "Keywords"
|
||||
// @Param tag_ids formData []string false "Tag IDs"
|
||||
// @Param image formData file false "Home image"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Success 201 {object} models.Home
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/homes [post]
|
||||
func (h *HomeHandler) CreateHome(c *gin.Context) {
|
||||
name := strings.TrimSpace(c.PostForm("name"))
|
||||
title := strings.TrimSpace(c.PostForm("title"))
|
||||
button1 := strings.TrimSpace(c.PostForm("button1"))
|
||||
button2 := strings.TrimSpace(c.PostForm("button2"))
|
||||
video := strings.TrimSpace(c.PostForm("video"))
|
||||
keywords := strings.TrimSpace(c.PostForm("keywords"))
|
||||
|
||||
if name == "" || title == "" || button1 == "" || button2 == "" || keywords == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name, title, button1, button2, and keywords are required"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := false
|
||||
if rawActive := strings.TrimSpace(c.PostForm("is_active")); rawActive != "" {
|
||||
parsed, err := strconv.ParseBool(rawActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
isActive = parsed
|
||||
}
|
||||
|
||||
tagIDs := parseTagIDs(c)
|
||||
|
||||
home, err := h.homeService.CreateHome(
|
||||
name,
|
||||
title,
|
||||
button1,
|
||||
button2,
|
||||
video,
|
||||
keywords,
|
||||
"",
|
||||
tagIDs,
|
||||
isActive,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if isHomeBadRequest(err) {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.FormFile("image")
|
||||
if err == nil {
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
imageURL, saveErr := utils.SaveOptimizedImage(file, "./uploads/homes", home.ID.String(), &utils.ImageOptions{
|
||||
Width: config.AppConfig.HomeImageWidth,
|
||||
Height: config.AppConfig.HomeImageHeight,
|
||||
Quality: float32(config.AppConfig.HomeImageQuality),
|
||||
Format: config.AppConfig.HomeImageFormat,
|
||||
Mode: config.AppConfig.HomeImageMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updated, updateErr := h.homeService.UpdateHome(
|
||||
home.ID.String(),
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
&imageURL,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if updateErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": updateErr.Error()})
|
||||
return
|
||||
}
|
||||
home = updated
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, home)
|
||||
}
|
||||
|
||||
// UpdateHome godoc
|
||||
// @Summary Update a home (Admin)
|
||||
// @Description Update an existing home entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "Home ID"
|
||||
// @Param name formData string false "Name"
|
||||
// @Param title formData string false "Title"
|
||||
// @Param button1 formData string false "Button 1"
|
||||
// @Param button2 formData string false "Button 2"
|
||||
// @Param video formData string false "Video URL"
|
||||
// @Param keywords formData string false "Keywords"
|
||||
// @Param tag_ids formData []string false "Tag IDs"
|
||||
// @Param image formData file false "Home image"
|
||||
// @Param slug formData string false "Slug"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Success 200 {object} models.Home
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/homes/{id} [put]
|
||||
func (h *HomeHandler) UpdateHome(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
name, hasName := getOptionalFormValue(c, "name")
|
||||
title, hasTitle := getOptionalFormValue(c, "title")
|
||||
button1, hasButton1 := getOptionalFormValue(c, "button1")
|
||||
button2, hasButton2 := getOptionalFormValue(c, "button2")
|
||||
video, hasVideo := getOptionalFormValue(c, "video")
|
||||
keywords, hasKeywords := getOptionalFormValue(c, "keywords")
|
||||
slug, hasSlug := getOptionalFormValue(c, "slug")
|
||||
|
||||
var namePtr *string
|
||||
var titlePtr *string
|
||||
var button1Ptr *string
|
||||
var button2Ptr *string
|
||||
var videoPtr *string
|
||||
var keywordsPtr *string
|
||||
var slugPtr *string
|
||||
|
||||
if hasName {
|
||||
namePtr = &name
|
||||
}
|
||||
if hasTitle {
|
||||
titlePtr = &title
|
||||
}
|
||||
if hasButton1 {
|
||||
button1Ptr = &button1
|
||||
}
|
||||
if hasButton2 {
|
||||
button2Ptr = &button2
|
||||
}
|
||||
if hasVideo {
|
||||
videoPtr = &video
|
||||
}
|
||||
if hasKeywords {
|
||||
keywordsPtr = &keywords
|
||||
}
|
||||
if hasSlug {
|
||||
slugPtr = &slug
|
||||
}
|
||||
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
|
||||
var tagIDsPtr *[]string
|
||||
if hasTagIDs(c) {
|
||||
tagIDs := parseTagIDs(c)
|
||||
tagIDsPtr = &tagIDs
|
||||
}
|
||||
|
||||
var imagePtr *string
|
||||
file, fileErr := c.FormFile("image")
|
||||
if fileErr == nil {
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
home, fetchErr := h.homeService.GetHomeByID(id)
|
||||
if fetchErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if home.Image != "" && strings.HasPrefix(home.Image, "/uploads/") {
|
||||
oldPath := "." + home.Image
|
||||
_ = os.Remove(oldPath)
|
||||
}
|
||||
|
||||
imageURL, saveErr := utils.SaveOptimizedImage(file, "./uploads/homes", id, &utils.ImageOptions{
|
||||
Width: config.AppConfig.HomeImageWidth,
|
||||
Height: config.AppConfig.HomeImageHeight,
|
||||
Quality: float32(config.AppConfig.HomeImageQuality),
|
||||
Format: config.AppConfig.HomeImageFormat,
|
||||
Mode: config.AppConfig.HomeImageMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
imagePtr = &imageURL
|
||||
}
|
||||
|
||||
home, err := h.homeService.UpdateHome(
|
||||
id,
|
||||
namePtr,
|
||||
titlePtr,
|
||||
button1Ptr,
|
||||
button2Ptr,
|
||||
videoPtr,
|
||||
keywordsPtr,
|
||||
imagePtr,
|
||||
slugPtr,
|
||||
tagIDsPtr,
|
||||
isActivePtr,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "home not found" {
|
||||
status = http.StatusNotFound
|
||||
} else if isHomeBadRequest(err) {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, home)
|
||||
}
|
||||
|
||||
// DeleteHome godoc
|
||||
// @Summary Delete a home (Admin)
|
||||
// @Description Delete a home by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Home ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/homes/{id} [delete]
|
||||
func (h *HomeHandler) DeleteHome(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.homeService.DeleteHome(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Home deleted successfully"})
|
||||
}
|
||||
|
||||
// AdminUploadHomeImage godoc
|
||||
// @Summary Upload home image (Admin)
|
||||
// @Description Upload an image for a specific home entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "Home ID"
|
||||
// @Param image formData file true "Home image"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/homes/{id}/image [post]
|
||||
func (h *HomeHandler) AdminUploadHomeImage(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
file, err := c.FormFile("image")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"})
|
||||
return
|
||||
}
|
||||
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
home, err := h.homeService.GetHomeByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if home.Image != "" && strings.HasPrefix(home.Image, "/uploads/") {
|
||||
oldPath := "." + home.Image
|
||||
_ = os.Remove(oldPath)
|
||||
}
|
||||
|
||||
imageURL, err := utils.SaveOptimizedImage(file, "./uploads/homes", id, &utils.ImageOptions{
|
||||
Width: config.AppConfig.HomeImageWidth,
|
||||
Height: config.AppConfig.HomeImageHeight,
|
||||
Quality: float32(config.AppConfig.HomeImageQuality),
|
||||
Format: config.AppConfig.HomeImageFormat,
|
||||
Mode: config.AppConfig.HomeImageMode,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := h.homeService.UpdateHome(
|
||||
id,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
&imageURL,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Home image uploaded successfully",
|
||||
"image_url": imageURL,
|
||||
"home": updated,
|
||||
})
|
||||
}
|
||||
|
||||
func isHomeBadRequest(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch err.Error() {
|
||||
case "slug already exists", "slug cannot be empty", "one or more tags not found":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func parseTagIDs(c *gin.Context) []string {
|
||||
tagIDs := c.PostFormArray("tag_ids")
|
||||
if len(tagIDs) == 0 {
|
||||
raw := strings.TrimSpace(c.PostForm("tag_ids"))
|
||||
if raw == "" {
|
||||
return tagIDs
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
tagIDs = append(tagIDs, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
return tagIDs
|
||||
}
|
||||
|
||||
func hasTagIDs(c *gin.Context) bool {
|
||||
if len(c.PostFormArray("tag_ids")) > 0 {
|
||||
return true
|
||||
}
|
||||
if raw := strings.TrimSpace(c.PostForm("tag_ids")); raw != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
215
api/handlers/knowledge_handler.go
Normal file
215
api/handlers/knowledge_handler.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type KnowledgeHandler struct {
|
||||
knowledgeService *services.KnowledgeService
|
||||
}
|
||||
|
||||
func NewKnowledgeHandler(knowledgeService *services.KnowledgeService) *KnowledgeHandler {
|
||||
return &KnowledgeHandler{knowledgeService: knowledgeService}
|
||||
}
|
||||
|
||||
type CreateKnowledgeRequest struct {
|
||||
Title string `json:"title"`
|
||||
ResumeID string `json:"resume_id"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UpdateKnowledgeRequest struct {
|
||||
Title *string `json:"title"`
|
||||
ResumeID *string `json:"resume_id"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// GetAllKnowledges godoc
|
||||
// @Summary Get all active knowledges
|
||||
// @Description Retrieve a list of active knowledge entries
|
||||
// @Tags resume
|
||||
// @Produce json
|
||||
// @Param resume_id query string false "Resume ID"
|
||||
// @Success 200 {array} models.Knowledge
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /knowledges [get]
|
||||
func (h *KnowledgeHandler) GetAllKnowledges(c *gin.Context) {
|
||||
resumeID := strings.TrimSpace(c.Query("resume_id"))
|
||||
var resumeIDPtr *string
|
||||
if resumeID != "" {
|
||||
resumeIDPtr = &resumeID
|
||||
}
|
||||
items, err := h.knowledgeService.GetAllKnowledges(resumeIDPtr, true)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetAllKnowledges godoc
|
||||
// @Summary Get all knowledges (Admin)
|
||||
// @Description Retrieve a list of all knowledge entries including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param resume_id query string false "Resume ID"
|
||||
// @Success 200 {array} models.Knowledge
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/knowledges [get]
|
||||
func (h *KnowledgeHandler) AdminGetAllKnowledges(c *gin.Context) {
|
||||
resumeID := strings.TrimSpace(c.Query("resume_id"))
|
||||
var resumeIDPtr *string
|
||||
if resumeID != "" {
|
||||
resumeIDPtr = &resumeID
|
||||
}
|
||||
items, err := h.knowledgeService.GetAllKnowledges(resumeIDPtr, false)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetKnowledgeByID godoc
|
||||
// @Summary Get a knowledge by ID (Admin)
|
||||
// @Description Retrieve details of a specific knowledge entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Knowledge ID"
|
||||
// @Success 200 {object} models.Knowledge
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/knowledges/{id} [get]
|
||||
func (h *KnowledgeHandler) AdminGetKnowledgeByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.knowledgeService.GetKnowledgeByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateKnowledge godoc
|
||||
// @Summary Create a new knowledge (Admin)
|
||||
// @Description Create a new knowledge entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreateKnowledgeRequest true "Knowledge Request"
|
||||
// @Success 201 {object} models.Knowledge
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/knowledges [post]
|
||||
func (h *KnowledgeHandler) CreateKnowledge(c *gin.Context) {
|
||||
var req CreateKnowledgeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
req.Title = strings.TrimSpace(req.Title)
|
||||
if req.Title == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := false
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
resumeUUID, parseErr := parseUUIDPtr(req.ResumeID)
|
||||
if parseErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume id"})
|
||||
return
|
||||
}
|
||||
item, err := h.knowledgeService.CreateKnowledge(req.Title, resumeUUID, isActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateKnowledge godoc
|
||||
// @Summary Update a knowledge (Admin)
|
||||
// @Description Update an existing knowledge entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Knowledge ID"
|
||||
// @Param request body UpdateKnowledgeRequest true "Knowledge Request"
|
||||
// @Success 200 {object} models.Knowledge
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/knowledges/{id} [put]
|
||||
func (h *KnowledgeHandler) UpdateKnowledge(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req UpdateKnowledgeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title != nil {
|
||||
trimmed := strings.TrimSpace(*req.Title)
|
||||
req.Title = &trimmed
|
||||
}
|
||||
if req.ResumeID != nil {
|
||||
trimmed := strings.TrimSpace(*req.ResumeID)
|
||||
req.ResumeID = &trimmed
|
||||
}
|
||||
|
||||
var resumeUUIDPtr *uuid.UUID
|
||||
if req.ResumeID != nil {
|
||||
parsed, parseErr := parseUUIDPtr(*req.ResumeID)
|
||||
if parseErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume id"})
|
||||
return
|
||||
}
|
||||
resumeUUIDPtr = parsed
|
||||
}
|
||||
item, err := h.knowledgeService.UpdateKnowledge(id, req.Title, resumeUUIDPtr, req.IsActive)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "knowledge not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteKnowledge godoc
|
||||
// @Summary Delete a knowledge (Admin)
|
||||
// @Description Delete a knowledge by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Knowledge ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/knowledges/{id} [delete]
|
||||
func (h *KnowledgeHandler) DeleteKnowledge(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.knowledgeService.DeleteKnowledge(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Knowledge deleted successfully"})
|
||||
}
|
||||
231
api/handlers/main_menu_handler.go
Normal file
231
api/handlers/main_menu_handler.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package handlers
|
||||
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MainMenuHandler struct {
|
||||
mainMenuService *services.MainMenuService
|
||||
}
|
||||
|
||||
func NewMainMenuHandler(mainMenuService *services.MainMenuService) *MainMenuHandler {
|
||||
return &MainMenuHandler{mainMenuService: mainMenuService}
|
||||
}
|
||||
|
||||
type CreateMainMenuRequest struct {
|
||||
Home string `json:"home"`
|
||||
About string `json:"about"`
|
||||
Services string `json:"services"`
|
||||
Resume string `json:"resume"`
|
||||
Portfolio string `json:"portfolio"`
|
||||
Contact string `json:"contact"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UpdateMainMenuRequest struct {
|
||||
Home *string `json:"home"`
|
||||
About *string `json:"about"`
|
||||
Services *string `json:"services"`
|
||||
Resume *string `json:"resume"`
|
||||
Portfolio *string `json:"portfolio"`
|
||||
Contact *string `json:"contact"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// GetAllMainMenus godoc
|
||||
// @Summary Get all active main menus
|
||||
// @Description Retrieve a list of active main menu entries
|
||||
// @Tags menu
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.MainMenu
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /main-menu [get]
|
||||
func (h *MainMenuHandler) GetAllMainMenus(c *gin.Context) {
|
||||
items, err := h.mainMenuService.GetAllMainMenus(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// GetActiveMainMenu godoc
|
||||
// @Summary Get active main menu
|
||||
// @Description Retrieve the newest active main menu entry
|
||||
// @Tags menu
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.MainMenu
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /main-menu/active [get]
|
||||
func (h *MainMenuHandler) GetActiveMainMenu(c *gin.Context) {
|
||||
item, err := h.mainMenuService.GetFirstActiveMainMenu()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminGetAllMainMenus godoc
|
||||
// @Summary Get all main menus (Admin)
|
||||
// @Description Retrieve a list of all main menu entries including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.MainMenu
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/main-menu [get]
|
||||
func (h *MainMenuHandler) AdminGetAllMainMenus(c *gin.Context) {
|
||||
items, err := h.mainMenuService.GetAllMainMenus(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetMainMenuByID godoc
|
||||
// @Summary Get a main menu by ID (Admin)
|
||||
// @Description Retrieve details of a specific main menu entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Main Menu ID"
|
||||
// @Success 200 {object} models.MainMenu
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/main-menu/{id} [get]
|
||||
func (h *MainMenuHandler) AdminGetMainMenuByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.mainMenuService.GetMainMenuByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateMainMenu godoc
|
||||
// @Summary Create a new main menu (Admin)
|
||||
// @Description Create a new main menu entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreateMainMenuRequest true "Main Menu Request"
|
||||
// @Success 201 {object} models.MainMenu
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/main-menu [post]
|
||||
func (h *MainMenuHandler) CreateMainMenu(c *gin.Context) {
|
||||
var req CreateMainMenuRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
req.Home = strings.TrimSpace(req.Home)
|
||||
req.About = strings.TrimSpace(req.About)
|
||||
req.Services = strings.TrimSpace(req.Services)
|
||||
req.Resume = strings.TrimSpace(req.Resume)
|
||||
req.Portfolio = strings.TrimSpace(req.Portfolio)
|
||||
req.Contact = strings.TrimSpace(req.Contact)
|
||||
if req.Home == "" || req.About == "" || req.Services == "" || req.Resume == "" || req.Portfolio == "" || req.Contact == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "home, about, services, resume, portfolio, and contact are required"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := false
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
item, err := h.mainMenuService.CreateMainMenu(req.Home, req.About, req.Services, req.Resume, req.Portfolio, req.Contact, isActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateMainMenu godoc
|
||||
// @Summary Update a main menu (Admin)
|
||||
// @Description Update an existing main menu entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Main Menu ID"
|
||||
// @Param request body UpdateMainMenuRequest true "Main Menu Request"
|
||||
// @Success 200 {object} models.MainMenu
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/main-menu/{id} [put]
|
||||
func (h *MainMenuHandler) UpdateMainMenu(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req UpdateMainMenuRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Home != nil {
|
||||
trimmed := strings.TrimSpace(*req.Home)
|
||||
req.Home = &trimmed
|
||||
}
|
||||
if req.About != nil {
|
||||
trimmed := strings.TrimSpace(*req.About)
|
||||
req.About = &trimmed
|
||||
}
|
||||
if req.Services != nil {
|
||||
trimmed := strings.TrimSpace(*req.Services)
|
||||
req.Services = &trimmed
|
||||
}
|
||||
if req.Resume != nil {
|
||||
trimmed := strings.TrimSpace(*req.Resume)
|
||||
req.Resume = &trimmed
|
||||
}
|
||||
if req.Portfolio != nil {
|
||||
trimmed := strings.TrimSpace(*req.Portfolio)
|
||||
req.Portfolio = &trimmed
|
||||
}
|
||||
if req.Contact != nil {
|
||||
trimmed := strings.TrimSpace(*req.Contact)
|
||||
req.Contact = &trimmed
|
||||
}
|
||||
|
||||
item, err := h.mainMenuService.UpdateMainMenu(id, req.Home, req.About, req.Services, req.Resume, req.Portfolio, req.Contact, req.IsActive)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "main menu not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteMainMenu godoc
|
||||
// @Summary Delete a main menu (Admin)
|
||||
// @Description Delete a main menu by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Main Menu ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/main-menu/{id} [delete]
|
||||
func (h *MainMenuHandler) DeleteMainMenu(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.mainMenuService.DeleteMainMenu(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Main menu deleted successfully"})
|
||||
}
|
||||
359
api/handlers/post_category_handler.go
Normal file
359
api/handlers/post_category_handler.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gauth-central/config"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PostCategoryHandler struct {
|
||||
categoryService *services.PostCategoryService
|
||||
}
|
||||
|
||||
func NewPostCategoryHandler(categoryService *services.PostCategoryService) *PostCategoryHandler {
|
||||
return &PostCategoryHandler{categoryService: categoryService}
|
||||
}
|
||||
|
||||
// GetAllPostCategories godoc
|
||||
// @Summary Get all active post categories
|
||||
// @Description Retrieve a list of active post categories
|
||||
// @Tags post-categories
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.PostCategory
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /post-categories [get]
|
||||
// GetAllPostCategories returns active post categories.
|
||||
func (h *PostCategoryHandler) GetAllPostCategories(c *gin.Context) {
|
||||
items, err := h.categoryService.GetAllPostCategories(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// GetPostCategoryBySlug godoc
|
||||
// @Summary Get post category by slug
|
||||
// @Description Retrieve an active post category by slug
|
||||
// @Tags post-categories
|
||||
// @Produce json
|
||||
// @Param slug path string true "Category Slug"
|
||||
// @Success 200 {object} models.PostCategory
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /post-categories/{slug} [get]
|
||||
// GetPostCategoryBySlug returns a post category by slug.
|
||||
func (h *PostCategoryHandler) GetPostCategoryBySlug(c *gin.Context) {
|
||||
slug := c.Param("slug")
|
||||
item, err := h.categoryService.GetPostCategoryBySlug(slug, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminGetAllPostCategories godoc
|
||||
// @Summary Get all post categories (Admin)
|
||||
// @Description Retrieve a list of all post categories including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.PostCategory
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/post-categories [get]
|
||||
// AdminGetAllPostCategories returns all categories.
|
||||
func (h *PostCategoryHandler) AdminGetAllPostCategories(c *gin.Context) {
|
||||
items, err := h.categoryService.GetAllPostCategories(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetPostCategoryByID godoc
|
||||
// @Summary Get post category by ID (Admin)
|
||||
// @Description Retrieve a post category by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Category ID"
|
||||
// @Success 200 {object} models.PostCategory
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/post-categories/{id} [get]
|
||||
// AdminGetPostCategoryByID returns a category by ID.
|
||||
func (h *PostCategoryHandler) AdminGetPostCategoryByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.categoryService.GetPostCategoryByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreatePostCategory godoc
|
||||
// @Summary Create a post category (Admin)
|
||||
// @Description Create a new post category
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param title formData string true "Title"
|
||||
// @Param keywords formData string true "Keywords"
|
||||
// @Param description formData string true "Description"
|
||||
// @Param order formData int false "Order"
|
||||
// @Param parent_id formData string false "Parent ID"
|
||||
// @Param image formData file false "Image"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Success 201 {object} models.PostCategory
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/post-categories [post]
|
||||
// CreatePostCategory creates a new category.
|
||||
func (h *PostCategoryHandler) CreatePostCategory(c *gin.Context) {
|
||||
title := strings.TrimSpace(c.PostForm("title"))
|
||||
keywords := strings.TrimSpace(c.PostForm("keywords"))
|
||||
description := strings.TrimSpace(c.PostForm("description"))
|
||||
if title == "" || keywords == "" || description == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "title, keywords, and description are required"})
|
||||
return
|
||||
}
|
||||
|
||||
order := 1
|
||||
if rawOrder := strings.TrimSpace(c.PostForm("order")); rawOrder != "" {
|
||||
parsed, err := strconv.Atoi(rawOrder)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "order must be an integer"})
|
||||
return
|
||||
}
|
||||
order = parsed
|
||||
}
|
||||
|
||||
parentID, err := parseUUIDPtr(c.PostForm("parent_id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := true
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
if isActivePtr != nil {
|
||||
isActive = *isActivePtr
|
||||
}
|
||||
|
||||
imageURL := ""
|
||||
file, fileErr := c.FormFile("image")
|
||||
if fileErr == nil {
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
saved, saveErr := utils.SaveOptimizedImage(file, "./uploads/post-categories", "category", &utils.ImageOptions{
|
||||
Width: config.AppConfig.PostCategoryImageWidth,
|
||||
Height: config.AppConfig.PostCategoryImageHeight,
|
||||
Quality: float32(config.AppConfig.PostCategoryImageQuality),
|
||||
Format: config.AppConfig.PostCategoryImageFormat,
|
||||
Mode: config.AppConfig.PostCategoryImageMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
imageURL = saved
|
||||
}
|
||||
|
||||
item, err := h.categoryService.CreatePostCategory(
|
||||
title,
|
||||
keywords,
|
||||
description,
|
||||
imageURL,
|
||||
order,
|
||||
parentID,
|
||||
isActive,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "slug cannot be empty" || err.Error() == "slug already exists" {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdatePostCategory godoc
|
||||
// @Summary Update a post category (Admin)
|
||||
// @Description Update an existing post category
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "Category ID"
|
||||
// @Param title formData string false "Title"
|
||||
// @Param keywords formData string false "Keywords"
|
||||
// @Param description formData string false "Description"
|
||||
// @Param order formData int false "Order"
|
||||
// @Param parent_id formData string false "Parent ID"
|
||||
// @Param image formData file false "Image"
|
||||
// @Param slug formData string false "Slug"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Success 200 {object} models.PostCategory
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/post-categories/{id} [put]
|
||||
// UpdatePostCategory updates a category.
|
||||
func (h *PostCategoryHandler) UpdatePostCategory(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
title, hasTitle := getOptionalFormValue(c, "title")
|
||||
keywords, hasKeywords := getOptionalFormValue(c, "keywords")
|
||||
description, hasDescription := getOptionalFormValue(c, "description")
|
||||
slug, hasSlug := getOptionalFormValue(c, "slug")
|
||||
|
||||
var titlePtr *string
|
||||
var keywordsPtr *string
|
||||
var descriptionPtr *string
|
||||
var slugPtr *string
|
||||
|
||||
if hasTitle {
|
||||
titlePtr = &title
|
||||
}
|
||||
if hasKeywords {
|
||||
keywordsPtr = &keywords
|
||||
}
|
||||
if hasDescription {
|
||||
descriptionPtr = &description
|
||||
}
|
||||
if hasSlug {
|
||||
slugPtr = &slug
|
||||
}
|
||||
|
||||
var orderPtr *int
|
||||
if rawOrder, hasOrder := getOptionalFormValue(c, "order"); hasOrder {
|
||||
if rawOrder == "" {
|
||||
orderPtr = nil
|
||||
} else {
|
||||
parsed, err := strconv.Atoi(rawOrder)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "order must be an integer"})
|
||||
return
|
||||
}
|
||||
orderPtr = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
parentIDValue, hasParent := getOptionalFormValue(c, "parent_id")
|
||||
parentIDSet := false
|
||||
var parentIDPtr *uuid.UUID
|
||||
if hasParent {
|
||||
parentIDSet = true
|
||||
if parentIDValue == "" {
|
||||
parentIDPtr = nil
|
||||
} else {
|
||||
parsed, err := parseUUIDPtr(parentIDValue)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"})
|
||||
return
|
||||
}
|
||||
parentIDPtr = parsed
|
||||
}
|
||||
}
|
||||
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
|
||||
var imagePtr *string
|
||||
file, fileErr := c.FormFile("image")
|
||||
if fileErr == nil {
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
item, fetchErr := h.categoryService.GetPostCategoryByID(id)
|
||||
if fetchErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if item.Image != "" && strings.HasPrefix(item.Image, "/uploads/") {
|
||||
_ = os.Remove("." + item.Image)
|
||||
}
|
||||
|
||||
saved, saveErr := utils.SaveOptimizedImage(file, "./uploads/post-categories", id, &utils.ImageOptions{
|
||||
Width: config.AppConfig.PostCategoryImageWidth,
|
||||
Height: config.AppConfig.PostCategoryImageHeight,
|
||||
Quality: float32(config.AppConfig.PostCategoryImageQuality),
|
||||
Format: config.AppConfig.PostCategoryImageFormat,
|
||||
Mode: config.AppConfig.PostCategoryImageMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
imagePtr = &saved
|
||||
}
|
||||
|
||||
item, err := h.categoryService.UpdatePostCategory(
|
||||
id,
|
||||
titlePtr,
|
||||
keywordsPtr,
|
||||
descriptionPtr,
|
||||
imagePtr,
|
||||
orderPtr,
|
||||
parentIDPtr,
|
||||
parentIDSet,
|
||||
slugPtr,
|
||||
isActivePtr,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
switch err.Error() {
|
||||
case "post category not found":
|
||||
status = http.StatusNotFound
|
||||
case "slug cannot be empty", "slug already exists":
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeletePostCategory godoc
|
||||
// @Summary Delete a post category (Admin)
|
||||
// @Description Delete a post category by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Category ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/post-categories/{id} [delete]
|
||||
// DeletePostCategory deletes a category by ID.
|
||||
func (h *PostCategoryHandler) DeletePostCategory(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.categoryService.DeletePostCategory(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Post category deleted successfully"})
|
||||
}
|
||||
74
api/handlers/post_category_view_handler.go
Normal file
74
api/handlers/post_category_view_handler.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PostCategoryViewHandler struct {
|
||||
viewService *services.PostCategoryViewService
|
||||
}
|
||||
|
||||
func NewPostCategoryViewHandler(viewService *services.PostCategoryViewService) *PostCategoryViewHandler {
|
||||
return &PostCategoryViewHandler{viewService: viewService}
|
||||
}
|
||||
|
||||
// TrackPostCategoryView godoc
|
||||
// @Summary Track post category view
|
||||
// @Description Record a post category view (daily per IP)
|
||||
// @Tags post-category-views
|
||||
// @Produce json
|
||||
// @Param id path string true "Category ID"
|
||||
// @Success 200 {object} models.PostCategoryView
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /post-categories/{id}/views [post]
|
||||
// TrackPostCategoryView records a category view.
|
||||
func (h *PostCategoryViewHandler) TrackPostCategoryView(c *gin.Context) {
|
||||
categoryID := c.Param("id")
|
||||
ipAddress := strings.TrimSpace(c.ClientIP())
|
||||
userAgent := strings.TrimSpace(c.GetHeader("User-Agent"))
|
||||
|
||||
view, err := h.viewService.TrackView(categoryID, ipAddress, userAgent)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "invalid category_id" {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, view)
|
||||
}
|
||||
|
||||
// AdminGetPostCategoryViews godoc
|
||||
// @Summary Get post category views (Admin)
|
||||
// @Description Retrieve views for a category
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param category_id query string true "Category ID"
|
||||
// @Success 200 {array} models.PostCategoryView
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/post-category-views [get]
|
||||
// AdminGetPostCategoryViews returns views for a category.
|
||||
func (h *PostCategoryViewHandler) AdminGetPostCategoryViews(c *gin.Context) {
|
||||
categoryID := strings.TrimSpace(c.Query("category_id"))
|
||||
if categoryID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "category_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
views, err := h.viewService.GetViewsByCategory(categoryID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, views)
|
||||
}
|
||||
257
api/handlers/post_comment_handler.go
Normal file
257
api/handlers/post_comment_handler.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PostCommentHandler struct {
|
||||
commentService *services.PostCommentService
|
||||
}
|
||||
|
||||
func NewPostCommentHandler(commentService *services.PostCommentService) *PostCommentHandler {
|
||||
return &PostCommentHandler{commentService: commentService}
|
||||
}
|
||||
|
||||
type CreatePostCommentRequest struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Body string `json:"body" binding:"required"`
|
||||
ParentID *string `json:"parent_id"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UpdatePostCommentRequest struct {
|
||||
Title *string `json:"title"`
|
||||
Body *string `json:"body"`
|
||||
ParentID *string `json:"parent_id"`
|
||||
Slug *string `json:"slug"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// GetPostCommentsByPostID godoc
|
||||
// @Summary Get active post comments
|
||||
// @Description Retrieve active comments for a post
|
||||
// @Tags post-comments
|
||||
// @Produce json
|
||||
// @Param id path string true "Post ID"
|
||||
// @Success 200 {array} models.PostComment
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /posts/{id}/comments [get]
|
||||
// GetPostCommentsByPostID returns active comments for a post.
|
||||
func (h *PostCommentHandler) GetPostCommentsByPostID(c *gin.Context) {
|
||||
postID := c.Param("id")
|
||||
items, err := h.commentService.GetPostCommentsByPostID(postID, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// CreatePostComment godoc
|
||||
// @Summary Create a post comment
|
||||
// @Description Create a new comment for a post (auth required)
|
||||
// @Tags post-comments
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Post ID"
|
||||
// @Param request body CreatePostCommentRequest true "Comment Request"
|
||||
// @Success 201 {object} models.PostComment
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /posts/{id}/comments [post]
|
||||
// CreatePostComment creates a comment for a post (auth required).
|
||||
func (h *PostCommentHandler) CreatePostComment(c *gin.Context) {
|
||||
userID := strings.TrimSpace(c.GetString("user_id"))
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user_id"})
|
||||
return
|
||||
}
|
||||
|
||||
postUUID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid post_id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreatePostCommentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var parentID *uuid.UUID
|
||||
if req.ParentID != nil {
|
||||
parsed, err := uuid.Parse(strings.TrimSpace(*req.ParentID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"})
|
||||
return
|
||||
}
|
||||
parentID = &parsed
|
||||
}
|
||||
|
||||
isActive := true
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
item, err := h.commentService.CreatePostComment(
|
||||
userUUID,
|
||||
postUUID,
|
||||
req.Title,
|
||||
req.Body,
|
||||
parentID,
|
||||
isActive,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "slug cannot be empty" || err.Error() == "slug already exists" {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// AdminGetAllPostComments godoc
|
||||
// @Summary Get all post comments (Admin)
|
||||
// @Description Retrieve comments with optional post filter
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param post_id query string false "Post ID"
|
||||
// @Success 200 {array} models.PostComment
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/post-comments [get]
|
||||
// AdminGetAllPostComments returns comments with optional post filter.
|
||||
func (h *PostCommentHandler) AdminGetAllPostComments(c *gin.Context) {
|
||||
postID := strings.TrimSpace(c.Query("post_id"))
|
||||
var postIDPtr *string
|
||||
if postID != "" {
|
||||
postIDPtr = &postID
|
||||
}
|
||||
|
||||
items, err := h.commentService.GetAllPostComments(postIDPtr, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetPostCommentByID godoc
|
||||
// @Summary Get post comment by ID (Admin)
|
||||
// @Description Retrieve a post comment by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Comment ID"
|
||||
// @Success 200 {object} models.PostComment
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/post-comments/{id} [get]
|
||||
// AdminGetPostCommentByID returns a comment by ID.
|
||||
func (h *PostCommentHandler) AdminGetPostCommentByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.commentService.GetPostCommentByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminUpdatePostComment godoc
|
||||
// @Summary Update a post comment (Admin)
|
||||
// @Description Update an existing post comment
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Comment ID"
|
||||
// @Param request body UpdatePostCommentRequest true "Comment Request"
|
||||
// @Success 200 {object} models.PostComment
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/post-comments/{id} [put]
|
||||
// AdminUpdatePostComment updates a comment.
|
||||
func (h *PostCommentHandler) AdminUpdatePostComment(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var req UpdatePostCommentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
parentIDSet := false
|
||||
var parentIDPtr *uuid.UUID
|
||||
if req.ParentID != nil {
|
||||
parentIDSet = true
|
||||
if strings.TrimSpace(*req.ParentID) == "" {
|
||||
parentIDPtr = nil
|
||||
} else {
|
||||
parsed, err := uuid.Parse(strings.TrimSpace(*req.ParentID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"})
|
||||
return
|
||||
}
|
||||
parentIDPtr = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
item, err := h.commentService.UpdatePostComment(
|
||||
id,
|
||||
req.Title,
|
||||
req.Body,
|
||||
parentIDPtr,
|
||||
parentIDSet,
|
||||
req.Slug,
|
||||
req.IsActive,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
switch err.Error() {
|
||||
case "post comment not found":
|
||||
status = http.StatusNotFound
|
||||
case "slug cannot be empty", "slug already exists":
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminDeletePostComment godoc
|
||||
// @Summary Delete a post comment (Admin)
|
||||
// @Description Delete a post comment by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Comment ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/post-comments/{id} [delete]
|
||||
// AdminDeletePostComment deletes a comment.
|
||||
func (h *PostCommentHandler) AdminDeletePostComment(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.commentService.DeletePostComment(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Post comment deleted successfully"})
|
||||
}
|
||||
461
api/handlers/post_handler.go
Normal file
461
api/handlers/post_handler.go
Normal file
@@ -0,0 +1,461 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gauth-central/config"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PostHandler struct {
|
||||
postService *services.PostService
|
||||
}
|
||||
|
||||
func NewPostHandler(postService *services.PostService) *PostHandler {
|
||||
return &PostHandler{postService: postService}
|
||||
}
|
||||
|
||||
// GetAllPosts godoc
|
||||
// @Summary Get all active posts with pagination
|
||||
// @Description Retrieve a list of active posts with pagination. Use front=true for front posts only
|
||||
// @Tags posts
|
||||
// @Produce json
|
||||
// @Param front query bool false "Front posts only"
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param limit query int false "Items per page" default(10)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /posts [get]
|
||||
// GetAllPosts returns active posts with pagination. Optional query: front=true for front posts only.
|
||||
func (h *PostHandler) GetAllPosts(c *gin.Context) {
|
||||
onlyFront := false
|
||||
if raw := strings.TrimSpace(c.Query("front")); raw != "" {
|
||||
parsed, err := strconv.ParseBool(raw)
|
||||
if err == nil {
|
||||
onlyFront = parsed
|
||||
}
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
items, total, err := h.postService.GetAllPosts(true, onlyFront, page, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPostBySlug godoc
|
||||
// @Summary Get post by slug
|
||||
// @Description Retrieve an active post by slug
|
||||
// @Tags posts
|
||||
// @Produce json
|
||||
// @Param slug path string true "Post Slug"
|
||||
// @Success 200 {object} models.Post
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /posts/slug/{slug} [get]
|
||||
// GetPostBySlug returns a post by slug.
|
||||
func (h *PostHandler) GetPostBySlug(c *gin.Context) {
|
||||
slug := c.Param("slug")
|
||||
item, err := h.postService.GetPostBySlug(slug, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminGetAllPosts godoc
|
||||
// @Summary Get all posts (Admin)
|
||||
// @Description Retrieve a list of all posts including inactive ones with pagination
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param limit query int false "Items per page" default(10)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/posts [get]
|
||||
// AdminGetAllPosts returns all posts with pagination.
|
||||
func (h *PostHandler) AdminGetAllPosts(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
items, total, err := h.postService.GetAllPosts(false, false, page, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminGetPostByID godoc
|
||||
// @Summary Get post by ID (Admin)
|
||||
// @Description Retrieve a post by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Post ID"
|
||||
// @Success 200 {object} models.Post
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/posts/{id} [get]
|
||||
// AdminGetPostByID returns a post by ID.
|
||||
func (h *PostHandler) AdminGetPostByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.postService.GetPostByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreatePost godoc
|
||||
// @Summary Create a post (Admin)
|
||||
// @Description Create a new post
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param title formData string true "Title"
|
||||
// @Param content formData string false "Content"
|
||||
// @Param keywords formData string true "Keywords"
|
||||
// @Param video formData string false "Video"
|
||||
// @Param category_ids formData []string false "Category IDs"
|
||||
// @Param tag_ids formData []string false "Tag IDs"
|
||||
// @Param parent_id formData string false "Parent ID"
|
||||
// @Param image formData file false "Image"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Param is_front formData bool false "Is front"
|
||||
// @Success 201 {object} models.Post
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/posts [post]
|
||||
// CreatePost creates a new post.
|
||||
func (h *PostHandler) CreatePost(c *gin.Context) {
|
||||
title := strings.TrimSpace(c.PostForm("title"))
|
||||
content := strings.TrimSpace(c.PostForm("content"))
|
||||
keywords := strings.TrimSpace(c.PostForm("keywords"))
|
||||
video := strings.TrimSpace(c.PostForm("video"))
|
||||
if title == "" || keywords == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "title and keywords are required"})
|
||||
return
|
||||
}
|
||||
if video == "" {
|
||||
video = "none"
|
||||
}
|
||||
|
||||
parentID, err := parseUUIDPtr(c.PostForm("parent_id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := true
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
if isActivePtr != nil {
|
||||
isActive = *isActivePtr
|
||||
}
|
||||
|
||||
isFront := true
|
||||
isFrontPtr, err := parseOptionalBool(c, "is_front")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_front must be true or false"})
|
||||
return
|
||||
}
|
||||
if isFrontPtr != nil {
|
||||
isFront = *isFrontPtr
|
||||
}
|
||||
|
||||
categoryIDs := parseIDList(c, "category_ids")
|
||||
tagIDs := parseIDList(c, "tag_ids")
|
||||
|
||||
imageURL := ""
|
||||
file, fileErr := c.FormFile("image")
|
||||
if fileErr == nil {
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
saved, saveErr := utils.SaveOptimizedImage(file, "./uploads/posts", "post", &utils.ImageOptions{
|
||||
Width: config.AppConfig.PostImageWidth,
|
||||
Height: config.AppConfig.PostImageHeight,
|
||||
Quality: float32(config.AppConfig.PostImageQuality),
|
||||
Format: config.AppConfig.PostImageFormat,
|
||||
Mode: config.AppConfig.PostImageMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
imageURL = saved
|
||||
}
|
||||
|
||||
item, err := h.postService.CreatePost(
|
||||
title,
|
||||
content,
|
||||
keywords,
|
||||
imageURL,
|
||||
video,
|
||||
categoryIDs,
|
||||
tagIDs,
|
||||
parentID,
|
||||
isActive,
|
||||
isFront,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
switch err.Error() {
|
||||
case "slug cannot be empty", "slug already exists", "one or more categories not found", "one or more tags not found":
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdatePost godoc
|
||||
// @Summary Update a post (Admin)
|
||||
// @Description Update an existing post
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "Post ID"
|
||||
// @Param title formData string false "Title"
|
||||
// @Param content formData string false "Content"
|
||||
// @Param keywords formData string false "Keywords"
|
||||
// @Param video formData string false "Video"
|
||||
// @Param category_ids formData []string false "Category IDs"
|
||||
// @Param tag_ids formData []string false "Tag IDs"
|
||||
// @Param parent_id formData string false "Parent ID"
|
||||
// @Param image formData file false "Image"
|
||||
// @Param slug formData string false "Slug"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Param is_front formData bool false "Is front"
|
||||
// @Success 200 {object} models.Post
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/posts/{id} [put]
|
||||
// UpdatePost updates a post.
|
||||
func (h *PostHandler) UpdatePost(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
title, hasTitle := getOptionalFormValue(c, "title")
|
||||
content, hasContent := getOptionalFormValue(c, "content")
|
||||
keywords, hasKeywords := getOptionalFormValue(c, "keywords")
|
||||
video, hasVideo := getOptionalFormValue(c, "video")
|
||||
slug, hasSlug := getOptionalFormValue(c, "slug")
|
||||
|
||||
var titlePtr *string
|
||||
var contentPtr *string
|
||||
var keywordsPtr *string
|
||||
var videoPtr *string
|
||||
var slugPtr *string
|
||||
|
||||
if hasTitle {
|
||||
titlePtr = &title
|
||||
}
|
||||
if hasContent {
|
||||
contentPtr = &content
|
||||
}
|
||||
if hasKeywords {
|
||||
keywordsPtr = &keywords
|
||||
}
|
||||
if hasVideo {
|
||||
videoPtr = &video
|
||||
}
|
||||
if hasSlug {
|
||||
slugPtr = &slug
|
||||
}
|
||||
|
||||
parentIDValue, hasParent := getOptionalFormValue(c, "parent_id")
|
||||
parentIDSet := false
|
||||
var parentIDPtr *uuid.UUID
|
||||
if hasParent {
|
||||
parentIDSet = true
|
||||
if parentIDValue == "" {
|
||||
parentIDPtr = nil
|
||||
} else {
|
||||
parsed, err := parseUUIDPtr(parentIDValue)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"})
|
||||
return
|
||||
}
|
||||
parentIDPtr = parsed
|
||||
}
|
||||
}
|
||||
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
|
||||
isFrontPtr, err := parseOptionalBool(c, "is_front")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_front must be true or false"})
|
||||
return
|
||||
}
|
||||
|
||||
var categoryIDsPtr *[]string
|
||||
if hasIDList(c, "category_ids") {
|
||||
ids := parseIDList(c, "category_ids")
|
||||
categoryIDsPtr = &ids
|
||||
}
|
||||
|
||||
var tagIDsPtr *[]string
|
||||
if hasIDList(c, "tag_ids") {
|
||||
ids := parseIDList(c, "tag_ids")
|
||||
tagIDsPtr = &ids
|
||||
}
|
||||
|
||||
var imagePtr *string
|
||||
file, fileErr := c.FormFile("image")
|
||||
if fileErr == nil {
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
item, fetchErr := h.postService.GetPostByID(id)
|
||||
if fetchErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if item.Image != "" && strings.HasPrefix(item.Image, "/uploads/") {
|
||||
_ = os.Remove("." + item.Image)
|
||||
}
|
||||
|
||||
saved, saveErr := utils.SaveOptimizedImage(file, "./uploads/posts", id, &utils.ImageOptions{
|
||||
Width: config.AppConfig.PostImageWidth,
|
||||
Height: config.AppConfig.PostImageHeight,
|
||||
Quality: float32(config.AppConfig.PostImageQuality),
|
||||
Format: config.AppConfig.PostImageFormat,
|
||||
Mode: config.AppConfig.PostImageMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
imagePtr = &saved
|
||||
}
|
||||
|
||||
item, err := h.postService.UpdatePost(
|
||||
id,
|
||||
titlePtr,
|
||||
contentPtr,
|
||||
keywordsPtr,
|
||||
imagePtr,
|
||||
videoPtr,
|
||||
categoryIDsPtr,
|
||||
tagIDsPtr,
|
||||
parentIDPtr,
|
||||
parentIDSet,
|
||||
slugPtr,
|
||||
isActivePtr,
|
||||
isFrontPtr,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
switch err.Error() {
|
||||
case "post not found":
|
||||
status = http.StatusNotFound
|
||||
case "slug cannot be empty", "slug already exists", "one or more categories not found", "one or more tags not found":
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeletePost godoc
|
||||
// @Summary Delete a post (Admin)
|
||||
// @Description Delete a post by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Post ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/posts/{id} [delete]
|
||||
// DeletePost deletes a post by ID.
|
||||
func (h *PostHandler) DeletePost(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.postService.DeletePost(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Post deleted successfully"})
|
||||
}
|
||||
|
||||
func parseIDList(c *gin.Context, key string) []string {
|
||||
ids := c.PostFormArray(key)
|
||||
if len(ids) == 0 {
|
||||
raw := strings.TrimSpace(c.PostForm(key))
|
||||
if raw == "" {
|
||||
return ids
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
ids = append(ids, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func hasIDList(c *gin.Context, key string) bool {
|
||||
if len(c.PostFormArray(key)) > 0 {
|
||||
return true
|
||||
}
|
||||
if raw := strings.TrimSpace(c.PostForm(key)); raw != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
162
api/handlers/post_tag_handler.go
Normal file
162
api/handlers/post_tag_handler.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PostTagHandler struct {
|
||||
postTagService *services.PostTagService
|
||||
}
|
||||
|
||||
func NewPostTagHandler(postTagService *services.PostTagService) *PostTagHandler {
|
||||
return &PostTagHandler{postTagService: postTagService}
|
||||
}
|
||||
|
||||
type CreatePostTagRequest struct {
|
||||
Tag string `json:"tag" binding:"required"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UpdatePostTagRequest struct {
|
||||
Tag string `json:"tag"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// GetAllPostTags godoc
|
||||
// @Summary Get all active post tags
|
||||
// @Description Retrieve a list of active post tags
|
||||
// @Tags post-tags
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.PostTag
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /post-tags [get]
|
||||
func (h *PostTagHandler) GetAllPostTags(c *gin.Context) {
|
||||
tags, err := h.postTagService.GetAllPostTags(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, tags)
|
||||
}
|
||||
|
||||
// AdminGetAllPostTags godoc
|
||||
// @Summary Get all post tags (Admin)
|
||||
// @Description Retrieve a list of all post tags including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.PostTag
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/post-tags [get]
|
||||
func (h *PostTagHandler) AdminGetAllPostTags(c *gin.Context) {
|
||||
tags, err := h.postTagService.GetAllPostTags(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, tags)
|
||||
}
|
||||
|
||||
// CreatePostTag godoc
|
||||
// @Summary Create a post tag (Admin)
|
||||
// @Description Create a new post tag
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreatePostTagRequest true "Post Tag Request"
|
||||
// @Success 201 {object} models.PostTag
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/post-tags [post]
|
||||
func (h *PostTagHandler) CreatePostTag(c *gin.Context) {
|
||||
var req CreatePostTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := true
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
tag, err := h.postTagService.CreatePostTag(req.Tag, isActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, tag)
|
||||
}
|
||||
|
||||
// UpdatePostTag godoc
|
||||
// @Summary Update a post tag (Admin)
|
||||
// @Description Update an existing post tag
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Post Tag ID"
|
||||
// @Param request body UpdatePostTagRequest true "Post Tag Request"
|
||||
// @Success 200 {object} models.PostTag
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/post-tags/{id} [put]
|
||||
func (h *PostTagHandler) UpdatePostTag(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req UpdatePostTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := h.postTagService.UpdatePostTag(id, req.Tag, req.IsActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tag)
|
||||
}
|
||||
|
||||
// DeletePostTag godoc
|
||||
// @Summary Delete a post tag (Admin)
|
||||
// @Description Delete a post tag by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Post Tag ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/post-tags/{id} [delete]
|
||||
func (h *PostTagHandler) DeletePostTag(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.postTagService.DeletePostTag(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Post tag deleted successfully"})
|
||||
}
|
||||
|
||||
// GetPostTagByID godoc
|
||||
// @Summary Get a post tag by ID (Admin)
|
||||
// @Description Retrieve details of a specific post tag
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Post Tag ID"
|
||||
// @Success 200 {object} models.PostTag
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/post-tags/{id} [get]
|
||||
func (h *PostTagHandler) GetPostTagByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
tag, err := h.postTagService.GetPostTagByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, tag)
|
||||
}
|
||||
326
api/handlers/profile_handler.go
Normal file
326
api/handlers/profile_handler.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/database"
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type ProfileHandler struct{}
|
||||
|
||||
func NewProfileHandler() *ProfileHandler {
|
||||
return &ProfileHandler{}
|
||||
}
|
||||
|
||||
// isOAuthUser checks if user is an OAuth user (has social accounts)
|
||||
func isOAuthUser(user *models.User) bool {
|
||||
// OAuth user if they have social accounts OR if they don't have a password
|
||||
return len(user.SocialAccounts) > 0 || user.Password == ""
|
||||
}
|
||||
|
||||
// GetProfile godoc
|
||||
// @Summary Get current user profile
|
||||
// @Tags Profile
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Router /profile [get]
|
||||
func (h *ProfileHandler) GetProfile(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.
|
||||
Preload("Roles").
|
||||
Preload("Roles.Permissions").
|
||||
Preload("SocialAccounts").
|
||||
Where("id = ?", userID).
|
||||
First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Add is_oauth_user flag to response
|
||||
type ProfileResponse struct {
|
||||
models.User
|
||||
IsOAuthUser bool `json:"is_oauth_user"`
|
||||
}
|
||||
|
||||
response := ProfileResponse{
|
||||
User: user,
|
||||
IsOAuthUser: isOAuthUser(&user),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// UpdateProfile godoc
|
||||
// @Summary Update current user profile
|
||||
// @Tags Profile
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param user_name formData string false "Username"
|
||||
// @Param avatar formData file false "Avatar image"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /profile [put]
|
||||
func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Try to parse as multipart form first
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
isMultipart := strings.Contains(contentType, "multipart/form-data")
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
if isMultipart {
|
||||
// Parse multipart form
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse form data"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get form values
|
||||
if userName := c.PostForm("user_name"); userName != "" {
|
||||
updates["user_name"] = userName
|
||||
}
|
||||
} else {
|
||||
// Parse as JSON
|
||||
var input struct {
|
||||
UserName *string `json:"user_name"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if input.UserName != nil {
|
||||
updates["user_name"] = *input.UserName
|
||||
}
|
||||
}
|
||||
|
||||
// Update basic user fields
|
||||
if len(updates) > 0 {
|
||||
if err := database.DB.Model(&models.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle avatar upload if multipart and file provided
|
||||
if isMultipart {
|
||||
avatarFile, err := c.FormFile("avatar")
|
||||
if err == nil && avatarFile != nil {
|
||||
// Validate file size (max 5MB)
|
||||
if avatarFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Avatar file size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user to check for old avatar
|
||||
var user models.User
|
||||
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete old avatar if exists and is local
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") {
|
||||
oldPath := "." + user.Avatar
|
||||
os.Remove(oldPath)
|
||||
}
|
||||
|
||||
// Use utils.SaveOptimizedImage
|
||||
avatarURL, err := utils.SaveOptimizedImage(avatarFile, "./uploads/avatars", userID, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update user avatar
|
||||
if err := database.DB.Model(&user).Update("avatar", avatarURL).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update avatar in database"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get updated user to return
|
||||
var user models.User
|
||||
if err := database.DB.
|
||||
Preload("Roles").
|
||||
Preload("SocialAccounts").
|
||||
Where("id = ?", userID).
|
||||
First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Profile updated successfully"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Profile updated successfully",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangePassword godoc
|
||||
// @Summary Change password
|
||||
// @Tags Profile
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body object true "Password change request"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /profile/password [put]
|
||||
func (h *ProfileHandler) ChangePassword(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
CurrentPassword string `json:"current_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user
|
||||
var user models.User
|
||||
if err := database.DB.Preload("SocialAccounts").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is OAuth user
|
||||
if isOAuthUser(&user) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change password for OAuth users (Google/GitHub login)"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.CurrentPassword)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Current password is incorrect"})
|
||||
return
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update password
|
||||
if err := database.DB.Model(&user).Update("password", string(hashedPassword)).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update password"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Password changed successfully"})
|
||||
}
|
||||
|
||||
// ChangeEmail godoc
|
||||
// @Summary Change email address
|
||||
// @Tags Profile
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body object true "Email change request"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /profile/email [put]
|
||||
func (h *ProfileHandler) ChangeEmail(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
NewEmail string `json:"new_email" binding:"required,email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user
|
||||
var user models.User
|
||||
if err := database.DB.Preload("SocialAccounts").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is OAuth user
|
||||
if isOAuthUser(&user) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change email for OAuth users (Google/GitHub login)"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Password is incorrect"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if new email already exists
|
||||
var existingUser models.User
|
||||
if err := database.DB.Where("email = ? AND id != ?", input.NewEmail, userID).First(&existingUser).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Email already in use"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate verification token
|
||||
verifyToken, err := utils.GenerateSecureToken(32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate verification token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update email and set as unverified
|
||||
falseBool := false
|
||||
updates := map[string]interface{}{
|
||||
"email": input.NewEmail,
|
||||
"email_verified": &falseBool,
|
||||
"email_verify_token": verifyToken,
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&user).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update email"})
|
||||
return
|
||||
}
|
||||
|
||||
// Send verification email
|
||||
go func() {
|
||||
if err := utils.SendVerificationEmail(input.NewEmail, verifyToken); err != nil {
|
||||
fmt.Printf("Failed to send verification email to %s: %v\n", input.NewEmail, err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Email updated. Please verify your new email address.",
|
||||
"new_email": input.NewEmail,
|
||||
"verification_token": verifyToken,
|
||||
})
|
||||
}
|
||||
244
api/handlers/resume_handler.go
Normal file
244
api/handlers/resume_handler.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package handlers
|
||||
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ResumeHandler struct {
|
||||
resumeService *services.ResumeService
|
||||
}
|
||||
|
||||
func NewResumeHandler(resumeService *services.ResumeService) *ResumeHandler {
|
||||
return &ResumeHandler{resumeService: resumeService}
|
||||
}
|
||||
|
||||
type CreateResumeRequest struct {
|
||||
Title string `json:"title"`
|
||||
TitleSub string `json:"title_sub"`
|
||||
Education string `json:"education"`
|
||||
Experience string `json:"experience"`
|
||||
CodingSkills string `json:"coding_skills"`
|
||||
Knowledge string `json:"knowledge"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UpdateResumeRequest struct {
|
||||
Title *string `json:"title"`
|
||||
TitleSub *string `json:"title_sub"`
|
||||
Education *string `json:"education"`
|
||||
Experience *string `json:"experience"`
|
||||
CodingSkills *string `json:"coding_skills"`
|
||||
Knowledge *string `json:"knowledge"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// GetAllResumes godoc
|
||||
// @Summary Get all active resumes
|
||||
// @Description Retrieve a list of active resumes
|
||||
// @Tags resume
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Resume
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /resumes [get]
|
||||
func (h *ResumeHandler) GetAllResumes(c *gin.Context) {
|
||||
items, err := h.resumeService.GetAllResumes(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// GetActiveResume godoc
|
||||
// @Summary Get active resume
|
||||
// @Description Retrieve the newest active resume entry
|
||||
// @Tags resume
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Resume
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /resumes/active [get]
|
||||
func (h *ResumeHandler) GetActiveResume(c *gin.Context) {
|
||||
item, err := h.resumeService.GetFirstActiveResume()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminGetAllResumes godoc
|
||||
// @Summary Get all resumes (Admin)
|
||||
// @Description Retrieve a list of all resumes including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Resume
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/resumes [get]
|
||||
func (h *ResumeHandler) AdminGetAllResumes(c *gin.Context) {
|
||||
items, err := h.resumeService.GetAllResumes(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetResumeByID godoc
|
||||
// @Summary Get a resume by ID (Admin)
|
||||
// @Description Retrieve details of a specific resume
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Resume ID"
|
||||
// @Success 200 {object} models.Resume
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/resumes/{id} [get]
|
||||
func (h *ResumeHandler) AdminGetResumeByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.resumeService.GetResumeByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateResume godoc
|
||||
// @Summary Create a new resume (Admin)
|
||||
// @Description Create a new resume entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreateResumeRequest true "Resume Request"
|
||||
// @Success 201 {object} models.Resume
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/resumes [post]
|
||||
func (h *ResumeHandler) CreateResume(c *gin.Context) {
|
||||
var req CreateResumeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
req.Title = strings.TrimSpace(req.Title)
|
||||
req.TitleSub = strings.TrimSpace(req.TitleSub)
|
||||
if req.Title == "" || req.TitleSub == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "title and title_sub are required"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := false
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
item, err := h.resumeService.CreateResume(
|
||||
req.Title,
|
||||
req.TitleSub,
|
||||
strings.TrimSpace(req.Education),
|
||||
strings.TrimSpace(req.Experience),
|
||||
strings.TrimSpace(req.CodingSkills),
|
||||
strings.TrimSpace(req.Knowledge),
|
||||
isActive,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateResume godoc
|
||||
// @Summary Update a resume (Admin)
|
||||
// @Description Update an existing resume entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Resume ID"
|
||||
// @Param request body UpdateResumeRequest true "Resume Request"
|
||||
// @Success 200 {object} models.Resume
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/resumes/{id} [put]
|
||||
func (h *ResumeHandler) UpdateResume(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req UpdateResumeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title != nil {
|
||||
trimmed := strings.TrimSpace(*req.Title)
|
||||
req.Title = &trimmed
|
||||
}
|
||||
if req.TitleSub != nil {
|
||||
trimmed := strings.TrimSpace(*req.TitleSub)
|
||||
req.TitleSub = &trimmed
|
||||
}
|
||||
if req.Education != nil {
|
||||
trimmed := strings.TrimSpace(*req.Education)
|
||||
req.Education = &trimmed
|
||||
}
|
||||
if req.Experience != nil {
|
||||
trimmed := strings.TrimSpace(*req.Experience)
|
||||
req.Experience = &trimmed
|
||||
}
|
||||
if req.CodingSkills != nil {
|
||||
trimmed := strings.TrimSpace(*req.CodingSkills)
|
||||
req.CodingSkills = &trimmed
|
||||
}
|
||||
if req.Knowledge != nil {
|
||||
trimmed := strings.TrimSpace(*req.Knowledge)
|
||||
req.Knowledge = &trimmed
|
||||
}
|
||||
|
||||
item, err := h.resumeService.UpdateResume(
|
||||
id,
|
||||
req.Title,
|
||||
req.TitleSub,
|
||||
req.Education,
|
||||
req.Experience,
|
||||
req.CodingSkills,
|
||||
req.Knowledge,
|
||||
req.IsActive,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "resume not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteResume godoc
|
||||
// @Summary Delete a resume (Admin)
|
||||
// @Description Delete a resume by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Resume ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/resumes/{id} [delete]
|
||||
func (h *ResumeHandler) DeleteResume(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.resumeService.DeleteResume(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Resume deleted successfully"})
|
||||
}
|
||||
287
api/handlers/service_handler.go
Normal file
287
api/handlers/service_handler.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gauth-central/config"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ServiceHandler struct {
|
||||
serviceService *services.ServiceService
|
||||
}
|
||||
|
||||
func NewServiceHandler(serviceService *services.ServiceService) *ServiceHandler {
|
||||
return &ServiceHandler{serviceService: serviceService}
|
||||
}
|
||||
|
||||
// GetAllServices godoc
|
||||
// @Summary Get all active services
|
||||
// @Description Retrieve a list of active services
|
||||
// @Tags services
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Service
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /services [get]
|
||||
func (h *ServiceHandler) GetAllServices(c *gin.Context) {
|
||||
items, err := h.serviceService.GetAllServices(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// GetServiceBySlug godoc
|
||||
// @Summary Get service by slug
|
||||
// @Description Retrieve a single active service by slug
|
||||
// @Tags services
|
||||
// @Produce json
|
||||
// @Param slug path string true "Service Slug"
|
||||
// @Success 200 {object} models.Service
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /services/{slug} [get]
|
||||
func (h *ServiceHandler) GetServiceBySlug(c *gin.Context) {
|
||||
slug := c.Param("slug")
|
||||
item, err := h.serviceService.GetServiceBySlug(slug, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminGetAllServices godoc
|
||||
// @Summary Get all services (Admin)
|
||||
// @Description Retrieve a list of all services including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Service
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/services [get]
|
||||
func (h *ServiceHandler) AdminGetAllServices(c *gin.Context) {
|
||||
items, err := h.serviceService.GetAllServices(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetServiceByID godoc
|
||||
// @Summary Get a service by ID (Admin)
|
||||
// @Description Retrieve details of a specific service
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Service ID"
|
||||
// @Success 200 {object} models.Service
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/services/{id} [get]
|
||||
func (h *ServiceHandler) AdminGetServiceByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.serviceService.GetServiceByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateService godoc
|
||||
// @Summary Create a new service (Admin)
|
||||
// @Description Create a new service entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param title formData string true "Title"
|
||||
// @Param content formData string false "Content"
|
||||
// @Param image formData file false "Image"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Success 201 {object} models.Service
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/services [post]
|
||||
func (h *ServiceHandler) CreateService(c *gin.Context) {
|
||||
title := strings.TrimSpace(c.PostForm("title"))
|
||||
content := strings.TrimSpace(c.PostForm("content"))
|
||||
if title == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := false
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
if isActivePtr != nil {
|
||||
isActive = *isActivePtr
|
||||
}
|
||||
|
||||
item, err := h.serviceService.CreateService(title, content, "", isActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
file, fileErr := c.FormFile("image")
|
||||
if fileErr == nil {
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
imageURL, saveErr := utils.SaveOptimizedImage(file, "./uploads/services", item.ID.String(), &utils.ImageOptions{
|
||||
Width: config.AppConfig.ServiceImageWidth,
|
||||
Height: config.AppConfig.ServiceImageHeight,
|
||||
Quality: float32(config.AppConfig.ServiceImageQuality),
|
||||
Format: config.AppConfig.ServiceImageFormat,
|
||||
Mode: config.AppConfig.ServiceImageMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updated, updateErr := h.serviceService.UpdateService(
|
||||
item.ID.String(),
|
||||
nil,
|
||||
nil,
|
||||
&imageURL,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if updateErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": updateErr.Error()})
|
||||
return
|
||||
}
|
||||
item = updated
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateService godoc
|
||||
// @Summary Update a service (Admin)
|
||||
// @Description Update an existing service entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "Service ID"
|
||||
// @Param title formData string false "Title"
|
||||
// @Param content formData string false "Content"
|
||||
// @Param image formData file false "Image"
|
||||
// @Param slug formData string false "Slug"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Success 200 {object} models.Service
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/services/{id} [put]
|
||||
func (h *ServiceHandler) UpdateService(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
title, hasTitle := getOptionalFormValue(c, "title")
|
||||
content, hasContent := getOptionalFormValue(c, "content")
|
||||
slug, hasSlug := getOptionalFormValue(c, "slug")
|
||||
|
||||
var titlePtr *string
|
||||
var contentPtr *string
|
||||
var slugPtr *string
|
||||
|
||||
if hasTitle {
|
||||
titlePtr = &title
|
||||
}
|
||||
if hasContent {
|
||||
contentPtr = &content
|
||||
}
|
||||
if hasSlug {
|
||||
slugPtr = &slug
|
||||
}
|
||||
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
|
||||
var imagePtr *string
|
||||
file, fileErr := c.FormFile("image")
|
||||
if fileErr == nil {
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
item, fetchErr := h.serviceService.GetServiceByID(id)
|
||||
if fetchErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if item.Image != "" && strings.HasPrefix(item.Image, "/uploads/") {
|
||||
oldPath := "." + item.Image
|
||||
_ = os.Remove(oldPath)
|
||||
}
|
||||
|
||||
imageURL, saveErr := utils.SaveOptimizedImage(file, "./uploads/services", id, &utils.ImageOptions{
|
||||
Width: config.AppConfig.ServiceImageWidth,
|
||||
Height: config.AppConfig.ServiceImageHeight,
|
||||
Quality: float32(config.AppConfig.ServiceImageQuality),
|
||||
Format: config.AppConfig.ServiceImageFormat,
|
||||
Mode: config.AppConfig.ServiceImageMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
imagePtr = &imageURL
|
||||
}
|
||||
|
||||
item, err := h.serviceService.UpdateService(
|
||||
id,
|
||||
titlePtr,
|
||||
contentPtr,
|
||||
imagePtr,
|
||||
slugPtr,
|
||||
isActivePtr,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "service not found" {
|
||||
status = http.StatusNotFound
|
||||
} else if err.Error() == "slug already exists" || err.Error() == "slug cannot be empty" {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteService godoc
|
||||
// @Summary Delete a service (Admin)
|
||||
// @Description Delete a service by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Service ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/services/{id} [delete]
|
||||
func (h *ServiceHandler) DeleteService(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.serviceService.DeleteService(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Service deleted successfully"})
|
||||
}
|
||||
198
api/handlers/service_title_handler.go
Normal file
198
api/handlers/service_title_handler.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ServiceTitleHandler struct {
|
||||
serviceTitleService *services.ServiceTitleService
|
||||
}
|
||||
|
||||
func NewServiceTitleHandler(serviceTitleService *services.ServiceTitleService) *ServiceTitleHandler {
|
||||
return &ServiceTitleHandler{serviceTitleService: serviceTitleService}
|
||||
}
|
||||
|
||||
// GetAllServiceTitles godoc
|
||||
// @Summary Get all active service titles
|
||||
// @Description Retrieve a list of active service titles
|
||||
// @Tags services
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.ServiceTitle
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /service-titles [get]
|
||||
func (h *ServiceTitleHandler) GetAllServiceTitles(c *gin.Context) {
|
||||
items, err := h.serviceTitleService.GetAllServiceTitles(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// GetActiveServiceTitle godoc
|
||||
// @Summary Get active service title
|
||||
// @Description Retrieve the newest active service title
|
||||
// @Tags services
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.ServiceTitle
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /service-titles/active [get]
|
||||
func (h *ServiceTitleHandler) GetActiveServiceTitle(c *gin.Context) {
|
||||
item, err := h.serviceTitleService.GetFirstActiveServiceTitle()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminGetAllServiceTitles godoc
|
||||
// @Summary Get all service titles (Admin)
|
||||
// @Description Retrieve a list of all service titles including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.ServiceTitle
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/service-titles [get]
|
||||
func (h *ServiceTitleHandler) AdminGetAllServiceTitles(c *gin.Context) {
|
||||
items, err := h.serviceTitleService.GetAllServiceTitles(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetServiceTitleByID godoc
|
||||
// @Summary Get a service title by ID (Admin)
|
||||
// @Description Retrieve details of a specific service title
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Service Title ID"
|
||||
// @Success 200 {object} models.ServiceTitle
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/service-titles/{id} [get]
|
||||
func (h *ServiceTitleHandler) AdminGetServiceTitleByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.serviceTitleService.GetServiceTitleByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateServiceTitle godoc
|
||||
// @Summary Create a new service title (Admin)
|
||||
// @Description Create a new service title entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body map[string]string true "Service Title Request"
|
||||
// @Success 201 {object} models.ServiceTitle
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/service-titles [post]
|
||||
func (h *ServiceTitleHandler) CreateServiceTitle(c *gin.Context) {
|
||||
var req struct {
|
||||
Title string `json:"title"`
|
||||
TitleSub string `json:"title_sub"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
req.Title = strings.TrimSpace(req.Title)
|
||||
req.TitleSub = strings.TrimSpace(req.TitleSub)
|
||||
if req.Title == "" || req.TitleSub == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "title and title_sub are required"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := false
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
item, err := h.serviceTitleService.CreateServiceTitle(req.Title, req.TitleSub, isActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateServiceTitle godoc
|
||||
// @Summary Update a service title (Admin)
|
||||
// @Description Update an existing service title entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Service Title ID"
|
||||
// @Param request body map[string]string true "Service Title Request"
|
||||
// @Success 200 {object} models.ServiceTitle
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/service-titles/{id} [put]
|
||||
func (h *ServiceTitleHandler) UpdateServiceTitle(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req struct {
|
||||
Title *string `json:"title"`
|
||||
TitleSub *string `json:"title_sub"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title != nil {
|
||||
trimmed := strings.TrimSpace(*req.Title)
|
||||
req.Title = &trimmed
|
||||
}
|
||||
if req.TitleSub != nil {
|
||||
trimmed := strings.TrimSpace(*req.TitleSub)
|
||||
req.TitleSub = &trimmed
|
||||
}
|
||||
|
||||
item, err := h.serviceTitleService.UpdateServiceTitle(id, req.Title, req.TitleSub, req.IsActive)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "service title not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteServiceTitle godoc
|
||||
// @Summary Delete a service title (Admin)
|
||||
// @Description Delete a service title by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Service Title ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/service-titles/{id} [delete]
|
||||
func (h *ServiceTitleHandler) DeleteServiceTitle(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.serviceTitleService.DeleteServiceTitle(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Service title deleted successfully"})
|
||||
}
|
||||
345
api/handlers/settings_handler.go
Normal file
345
api/handlers/settings_handler.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SettingsHandler struct {
|
||||
settingsService *services.SettingsService
|
||||
}
|
||||
|
||||
func NewSettingsHandler(settingsService *services.SettingsService) *SettingsHandler {
|
||||
return &SettingsHandler{
|
||||
settingsService: settingsService,
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CORS WHITELIST ====================
|
||||
|
||||
// GetAllWhitelist godoc
|
||||
// @Summary Get all CORS whitelist entries
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.CorsWhitelist
|
||||
// @Router /settings/cors/whitelist [get]
|
||||
func (h *SettingsHandler) GetAllWhitelist(c *gin.Context) {
|
||||
whitelists, err := h.settingsService.GetAllCorsWhitelist()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch whitelist"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, whitelists)
|
||||
}
|
||||
|
||||
// CreateWhitelist godoc
|
||||
// @Summary Create CORS whitelist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param whitelist body object true "Whitelist data"
|
||||
// @Success 201 {object} models.CorsWhitelist
|
||||
// @Router /settings/cors/whitelist [post]
|
||||
func (h *SettingsHandler) CreateWhitelist(c *gin.Context) {
|
||||
var input struct {
|
||||
Origin string `json:"origin" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
email := c.GetString("email")
|
||||
whitelist := &models.CorsWhitelist{
|
||||
Origin: input.Origin,
|
||||
Description: input.Description,
|
||||
IsActive: true,
|
||||
CreatedBy: email,
|
||||
}
|
||||
|
||||
err := h.settingsService.CreateCorsWhitelist(whitelist)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrCorsOriginExists) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Origin already exists"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create whitelist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, whitelist)
|
||||
}
|
||||
|
||||
// UpdateWhitelist godoc
|
||||
// @Summary Update CORS whitelist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Whitelist ID"
|
||||
// @Param whitelist body object true "Update data"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/cors/whitelist/{id} [put]
|
||||
func (h *SettingsHandler) UpdateWhitelist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var input struct {
|
||||
Origin *string `json:"origin"`
|
||||
Description *string `json:"description"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if input.Origin != nil {
|
||||
updates["origin"] = *input.Origin
|
||||
}
|
||||
if input.Description != nil {
|
||||
updates["description"] = *input.Description
|
||||
}
|
||||
if input.IsActive != nil {
|
||||
updates["is_active"] = *input.IsActive
|
||||
}
|
||||
|
||||
err := h.settingsService.UpdateCorsWhitelist(id, updates)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update whitelist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Whitelist updated successfully"})
|
||||
}
|
||||
|
||||
// DeleteWhitelist godoc
|
||||
// @Summary Delete CORS whitelist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Whitelist ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/cors/whitelist/{id} [delete]
|
||||
func (h *SettingsHandler) DeleteWhitelist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
err := h.settingsService.DeleteCorsWhitelist(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete whitelist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Whitelist entry deleted successfully"})
|
||||
}
|
||||
|
||||
// ==================== CORS BLACKLIST ====================
|
||||
|
||||
// GetAllBlacklist godoc
|
||||
// @Summary Get all CORS blacklist entries
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.CorsBlacklist
|
||||
// @Router /settings/cors/blacklist [get]
|
||||
func (h *SettingsHandler) GetAllBlacklist(c *gin.Context) {
|
||||
blacklists, err := h.settingsService.GetAllCorsBlacklist()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch blacklist"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, blacklists)
|
||||
}
|
||||
|
||||
// CreateBlacklist godoc
|
||||
// @Summary Create CORS blacklist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param blacklist body object true "Blacklist data"
|
||||
// @Success 201 {object} models.CorsBlacklist
|
||||
// @Router /settings/cors/blacklist [post]
|
||||
func (h *SettingsHandler) CreateBlacklist(c *gin.Context) {
|
||||
var input struct {
|
||||
Origin string `json:"origin" binding:"required"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
email := c.GetString("email")
|
||||
blacklist := &models.CorsBlacklist{
|
||||
Origin: input.Origin,
|
||||
Reason: input.Reason,
|
||||
IsActive: true,
|
||||
CreatedBy: email,
|
||||
}
|
||||
|
||||
err := h.settingsService.CreateCorsBlacklist(blacklist)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create blacklist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, blacklist)
|
||||
}
|
||||
|
||||
// UpdateBlacklist godoc
|
||||
// @Summary Update CORS blacklist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Blacklist ID"
|
||||
// @Param blacklist body object true "Update data"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/cors/blacklist/{id} [put]
|
||||
func (h *SettingsHandler) UpdateBlacklist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var input struct {
|
||||
Origin *string `json:"origin"`
|
||||
Reason *string `json:"reason"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if input.Origin != nil {
|
||||
updates["origin"] = *input.Origin
|
||||
}
|
||||
if input.Reason != nil {
|
||||
updates["reason"] = *input.Reason
|
||||
}
|
||||
if input.IsActive != nil {
|
||||
updates["is_active"] = *input.IsActive
|
||||
}
|
||||
|
||||
err := h.settingsService.UpdateCorsBlacklist(id, updates)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update blacklist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Blacklist updated successfully"})
|
||||
}
|
||||
|
||||
// DeleteBlacklist godoc
|
||||
// @Summary Delete CORS blacklist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Blacklist ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/cors/blacklist/{id} [delete]
|
||||
func (h *SettingsHandler) DeleteBlacklist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
err := h.settingsService.DeleteCorsBlacklist(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete blacklist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Blacklist entry deleted successfully"})
|
||||
}
|
||||
|
||||
// InvalidateCorsCache godoc
|
||||
// @Summary Invalidate CORS cache (whitelist + blacklist)
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/cors/cache/invalidate [post]
|
||||
func (h *SettingsHandler) InvalidateCorsCache(c *gin.Context) {
|
||||
h.settingsService.InvalidateCorsCache()
|
||||
c.JSON(http.StatusOK, gin.H{"message": "CORS cache invalidated"})
|
||||
}
|
||||
|
||||
// ==================== RATE LIMIT SETTINGS ====================
|
||||
|
||||
// GetAllRateLimits godoc
|
||||
// @Summary Get all rate limit settings
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.RateLimitSetting
|
||||
// @Router /settings/ratelimit [get]
|
||||
func (h *SettingsHandler) GetAllRateLimits(c *gin.Context) {
|
||||
settings, err := h.settingsService.GetAllRateLimitSettings()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch rate limit settings"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, settings)
|
||||
}
|
||||
|
||||
// UpdateRateLimit godoc
|
||||
// @Summary Update rate limit setting
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Setting ID"
|
||||
// @Param setting body object true "Update data"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/ratelimit/{id} [put]
|
||||
func (h *SettingsHandler) UpdateRateLimit(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var input struct {
|
||||
MaxRequests *int64 `json:"max_requests"`
|
||||
WindowSeconds *int `json:"window_seconds"`
|
||||
Description *string `json:"description"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
email := c.GetString("email")
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
if input.MaxRequests != nil {
|
||||
updates["max_requests"] = *input.MaxRequests
|
||||
}
|
||||
if input.WindowSeconds != nil {
|
||||
updates["window_seconds"] = *input.WindowSeconds
|
||||
}
|
||||
if input.Description != nil {
|
||||
updates["description"] = *input.Description
|
||||
}
|
||||
if input.IsActive != nil {
|
||||
updates["is_active"] = *input.IsActive
|
||||
}
|
||||
updates["updated_by"] = email
|
||||
|
||||
err := h.settingsService.UpdateRateLimitSetting(id, updates)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update rate limit setting"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Rate limit setting updated successfully"})
|
||||
}
|
||||
451
api/handlers/site_info_handler.go
Normal file
451
api/handlers/site_info_handler.go
Normal file
@@ -0,0 +1,451 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gauth-central/config"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SiteInfoHandler struct {
|
||||
siteInfoService *services.SiteInfoService
|
||||
}
|
||||
|
||||
func NewSiteInfoHandler(siteInfoService *services.SiteInfoService) *SiteInfoHandler {
|
||||
return &SiteInfoHandler{siteInfoService: siteInfoService}
|
||||
}
|
||||
|
||||
// GetAllSiteInfos godoc
|
||||
// @Summary Get all active site info
|
||||
// @Description Retrieve a list of active site info entries
|
||||
// @Tags site-info
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Setting
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /site-info [get]
|
||||
func (h *SiteInfoHandler) GetAllSiteInfos(c *gin.Context) {
|
||||
items, err := h.siteInfoService.GetAllSiteInfos(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// GetActiveSiteInfo godoc
|
||||
// @Summary Get active site info
|
||||
// @Description Retrieve the newest active site info entry
|
||||
// @Tags site-info
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Setting
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /site-info/active [get]
|
||||
func (h *SiteInfoHandler) GetActiveSiteInfo(c *gin.Context) {
|
||||
item, err := h.siteInfoService.GetFirstActiveSiteInfo()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminGetAllSiteInfos godoc
|
||||
// @Summary Get all site info (Admin)
|
||||
// @Description Retrieve a list of all site info entries including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Setting
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/site-info [get]
|
||||
func (h *SiteInfoHandler) AdminGetAllSiteInfos(c *gin.Context) {
|
||||
items, err := h.siteInfoService.GetAllSiteInfos(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetSiteInfoByID godoc
|
||||
// @Summary Get site info by ID (Admin)
|
||||
// @Description Retrieve details of a specific site info entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Site Info ID"
|
||||
// @Success 200 {object} models.Setting
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/site-info/{id} [get]
|
||||
func (h *SiteInfoHandler) AdminGetSiteInfoByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.siteInfoService.GetSiteInfoByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateSiteInfo godoc
|
||||
// @Summary Create site info (Admin)
|
||||
// @Description Create a new site info entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param title formData string true "Title"
|
||||
// @Param meta_title formData string false "Meta title"
|
||||
// @Param meta_description formData string false "Meta description"
|
||||
// @Param phone formData string true "Phone"
|
||||
// @Param url formData string false "Site 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 w_logo formData file false "White logo"
|
||||
// @Param b_logo formData file false "Black logo"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Success 201 {object} models.Setting
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/site-info [post]
|
||||
func (h *SiteInfoHandler) CreateSiteInfo(c *gin.Context) {
|
||||
title := strings.TrimSpace(c.PostForm("title"))
|
||||
phone := strings.TrimSpace(c.PostForm("phone"))
|
||||
email := strings.TrimSpace(c.PostForm("email"))
|
||||
if title == "" || phone == "" || email == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "title, phone, and email are required"})
|
||||
return
|
||||
}
|
||||
|
||||
metaTitle := strings.TrimSpace(c.PostForm("meta_title"))
|
||||
metaDescription := strings.TrimSpace(c.PostForm("meta_description"))
|
||||
url := strings.TrimSpace(c.PostForm("url"))
|
||||
facebook := strings.TrimSpace(c.PostForm("facebook"))
|
||||
xValue := strings.TrimSpace(c.PostForm("x"))
|
||||
instagram := strings.TrimSpace(c.PostForm("instagram"))
|
||||
whatsapp := strings.TrimSpace(c.PostForm("whatsapp"))
|
||||
pinterest := strings.TrimSpace(c.PostForm("pinterest"))
|
||||
linkedin := strings.TrimSpace(c.PostForm("linkedin"))
|
||||
slogan := strings.TrimSpace(c.PostForm("slogan"))
|
||||
address := strings.TrimSpace(c.PostForm("address"))
|
||||
copyright := strings.TrimSpace(c.PostForm("copyright"))
|
||||
mapEmbed := strings.TrimSpace(c.PostForm("map_embed"))
|
||||
|
||||
isActive := false
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
if isActivePtr != nil {
|
||||
isActive = *isActivePtr
|
||||
}
|
||||
|
||||
wLogo := ""
|
||||
wLogoFile, wLogoErr := c.FormFile("w_logo")
|
||||
if wLogoErr == nil {
|
||||
if wLogoFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "w_logo size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
wLogo, err = utils.SaveOptimizedImage(wLogoFile, "./uploads/logo", "wlogo", &utils.ImageOptions{
|
||||
Width: config.AppConfig.SettingsLogoWidth,
|
||||
Height: config.AppConfig.SettingsLogoHeight,
|
||||
Quality: float32(config.AppConfig.SettingsLogoQuality),
|
||||
Format: config.AppConfig.SettingsLogoFormat,
|
||||
Mode: config.AppConfig.SettingsLogoMode,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save w_logo: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bLogo := ""
|
||||
bLogoFile, bLogoErr := c.FormFile("b_logo")
|
||||
if bLogoErr == nil {
|
||||
if bLogoFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "b_logo size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
bLogo, err = utils.SaveOptimizedImage(bLogoFile, "./uploads/logo", "blogo", &utils.ImageOptions{
|
||||
Width: config.AppConfig.SettingsLogoWidth,
|
||||
Height: config.AppConfig.SettingsLogoHeight,
|
||||
Quality: float32(config.AppConfig.SettingsLogoQuality),
|
||||
Format: config.AppConfig.SettingsLogoFormat,
|
||||
Mode: config.AppConfig.SettingsLogoMode,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save b_logo: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
item, err := h.siteInfoService.CreateSiteInfo(
|
||||
title,
|
||||
metaTitle,
|
||||
metaDescription,
|
||||
phone,
|
||||
url,
|
||||
email,
|
||||
facebook,
|
||||
xValue,
|
||||
instagram,
|
||||
whatsapp,
|
||||
pinterest,
|
||||
linkedin,
|
||||
slogan,
|
||||
wLogo,
|
||||
bLogo,
|
||||
isActive,
|
||||
address,
|
||||
copyright,
|
||||
mapEmbed,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateSiteInfo godoc
|
||||
// @Summary Update site info (Admin)
|
||||
// @Description Update an existing site info entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "Site Info 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 "Site 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 w_logo formData file false "White logo"
|
||||
// @Param b_logo formData file false "Black logo"
|
||||
// @Param is_active formData bool false "Is active"
|
||||
// @Success 200 {object} models.Setting
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/site-info/{id} [put]
|
||||
func (h *SiteInfoHandler) UpdateSiteInfo(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
title, hasTitle := getOptionalFormValue(c, "title")
|
||||
metaTitle, hasMetaTitle := getOptionalFormValue(c, "meta_title")
|
||||
metaDescription, hasMetaDescription := getOptionalFormValue(c, "meta_description")
|
||||
phone, hasPhone := getOptionalFormValue(c, "phone")
|
||||
url, hasURL := getOptionalFormValue(c, "url")
|
||||
email, hasEmail := getOptionalFormValue(c, "email")
|
||||
facebook, hasFacebook := getOptionalFormValue(c, "facebook")
|
||||
xValue, hasX := getOptionalFormValue(c, "x")
|
||||
instagram, hasInstagram := getOptionalFormValue(c, "instagram")
|
||||
whatsapp, hasWhatsapp := getOptionalFormValue(c, "whatsapp")
|
||||
pinterest, hasPinterest := getOptionalFormValue(c, "pinterest")
|
||||
linkedin, hasLinkedin := getOptionalFormValue(c, "linkedin")
|
||||
slogan, hasSlogan := getOptionalFormValue(c, "slogan")
|
||||
address, hasAddress := getOptionalFormValue(c, "address")
|
||||
copyright, hasCopyright := getOptionalFormValue(c, "copyright")
|
||||
mapEmbed, hasMapEmbed := getOptionalFormValue(c, "map_embed")
|
||||
|
||||
var titlePtr *string
|
||||
var metaTitlePtr *string
|
||||
var metaDescriptionPtr *string
|
||||
var phonePtr *string
|
||||
var urlPtr *string
|
||||
var emailPtr *string
|
||||
var facebookPtr *string
|
||||
var xPtr *string
|
||||
var instagramPtr *string
|
||||
var whatsappPtr *string
|
||||
var pinterestPtr *string
|
||||
var linkedinPtr *string
|
||||
var sloganPtr *string
|
||||
var addressPtr *string
|
||||
var copyrightPtr *string
|
||||
var mapEmbedPtr *string
|
||||
|
||||
if hasTitle {
|
||||
titlePtr = &title
|
||||
}
|
||||
if hasMetaTitle {
|
||||
metaTitlePtr = &metaTitle
|
||||
}
|
||||
if hasMetaDescription {
|
||||
metaDescriptionPtr = &metaDescription
|
||||
}
|
||||
if hasPhone {
|
||||
phonePtr = &phone
|
||||
}
|
||||
if hasURL {
|
||||
urlPtr = &url
|
||||
}
|
||||
if hasEmail {
|
||||
emailPtr = &email
|
||||
}
|
||||
if hasFacebook {
|
||||
facebookPtr = &facebook
|
||||
}
|
||||
if hasX {
|
||||
xPtr = &xValue
|
||||
}
|
||||
if hasInstagram {
|
||||
instagramPtr = &instagram
|
||||
}
|
||||
if hasWhatsapp {
|
||||
whatsappPtr = &whatsapp
|
||||
}
|
||||
if hasPinterest {
|
||||
pinterestPtr = &pinterest
|
||||
}
|
||||
if hasLinkedin {
|
||||
linkedinPtr = &linkedin
|
||||
}
|
||||
if hasSlogan {
|
||||
sloganPtr = &slogan
|
||||
}
|
||||
if hasAddress {
|
||||
addressPtr = &address
|
||||
}
|
||||
if hasCopyright {
|
||||
copyrightPtr = ©right
|
||||
}
|
||||
if hasMapEmbed {
|
||||
mapEmbedPtr = &mapEmbed
|
||||
}
|
||||
|
||||
isActivePtr, err := parseOptionalBool(c, "is_active")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"})
|
||||
return
|
||||
}
|
||||
|
||||
var wLogoPtr *string
|
||||
wLogoFile, wLogoErr := c.FormFile("w_logo")
|
||||
if wLogoErr == nil {
|
||||
if wLogoFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "w_logo size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
item, fetchErr := h.siteInfoService.GetSiteInfoByID(id)
|
||||
if fetchErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
||||
return
|
||||
}
|
||||
if item.WLogo != "" && strings.HasPrefix(item.WLogo, "/uploads/") {
|
||||
_ = os.Remove("." + item.WLogo)
|
||||
}
|
||||
wLogo, saveErr := utils.SaveOptimizedImage(wLogoFile, "./uploads/logo", id+"_w", &utils.ImageOptions{
|
||||
Width: config.AppConfig.SettingsLogoWidth,
|
||||
Height: config.AppConfig.SettingsLogoHeight,
|
||||
Quality: float32(config.AppConfig.SettingsLogoQuality),
|
||||
Format: config.AppConfig.SettingsLogoFormat,
|
||||
Mode: config.AppConfig.SettingsLogoMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save w_logo: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
wLogoPtr = &wLogo
|
||||
}
|
||||
|
||||
var bLogoPtr *string
|
||||
bLogoFile, bLogoErr := c.FormFile("b_logo")
|
||||
if bLogoErr == nil {
|
||||
if bLogoFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "b_logo size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
item, fetchErr := h.siteInfoService.GetSiteInfoByID(id)
|
||||
if fetchErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()})
|
||||
return
|
||||
}
|
||||
if item.BLogo != "" && strings.HasPrefix(item.BLogo, "/uploads/") {
|
||||
_ = os.Remove("." + item.BLogo)
|
||||
}
|
||||
bLogo, saveErr := utils.SaveOptimizedImage(bLogoFile, "./uploads/logo", id+"_b", &utils.ImageOptions{
|
||||
Width: config.AppConfig.SettingsLogoWidth,
|
||||
Height: config.AppConfig.SettingsLogoHeight,
|
||||
Quality: float32(config.AppConfig.SettingsLogoQuality),
|
||||
Format: config.AppConfig.SettingsLogoFormat,
|
||||
Mode: config.AppConfig.SettingsLogoMode,
|
||||
})
|
||||
if saveErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save b_logo: " + saveErr.Error()})
|
||||
return
|
||||
}
|
||||
bLogoPtr = &bLogo
|
||||
}
|
||||
|
||||
item, err := h.siteInfoService.UpdateSiteInfo(
|
||||
id,
|
||||
titlePtr,
|
||||
metaTitlePtr,
|
||||
metaDescriptionPtr,
|
||||
phonePtr,
|
||||
urlPtr,
|
||||
emailPtr,
|
||||
facebookPtr,
|
||||
xPtr,
|
||||
instagramPtr,
|
||||
whatsappPtr,
|
||||
pinterestPtr,
|
||||
linkedinPtr,
|
||||
sloganPtr,
|
||||
wLogoPtr,
|
||||
bLogoPtr,
|
||||
isActivePtr,
|
||||
addressPtr,
|
||||
copyrightPtr,
|
||||
mapEmbedPtr,
|
||||
)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "site info not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteSiteInfo godoc
|
||||
// @Summary Delete site info (Admin)
|
||||
// @Description Delete a site info entry by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Site Info ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/site-info/{id} [delete]
|
||||
func (h *SiteInfoHandler) DeleteSiteInfo(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.siteInfoService.DeleteSiteInfo(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Site info deleted successfully"})
|
||||
}
|
||||
183
api/handlers/site_settings_handler.go
Normal file
183
api/handlers/site_settings_handler.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SiteSettingsHandler struct {
|
||||
siteSettingsService *services.SiteSettingsService
|
||||
}
|
||||
|
||||
func NewSiteSettingsHandler(siteSettingsService *services.SiteSettingsService) *SiteSettingsHandler {
|
||||
return &SiteSettingsHandler{siteSettingsService: siteSettingsService}
|
||||
}
|
||||
|
||||
// GetAllSiteSettings godoc
|
||||
// @Summary Get all active site settings
|
||||
// @Description Retrieve a list of active site settings
|
||||
// @Tags site-settings
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.SiteSettings
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /site-settings [get]
|
||||
func (h *SiteSettingsHandler) GetAllSiteSettings(c *gin.Context) {
|
||||
items, err := h.siteSettingsService.GetAllSiteSettings(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// GetActiveSiteSettings godoc
|
||||
// @Summary Get active site settings
|
||||
// @Description Retrieve the newest active site settings entry
|
||||
// @Tags site-settings
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.SiteSettings
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /site-settings/active [get]
|
||||
func (h *SiteSettingsHandler) GetActiveSiteSettings(c *gin.Context) {
|
||||
item, err := h.siteSettingsService.GetFirstActiveSiteSettings()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// AdminGetAllSiteSettings godoc
|
||||
// @Summary Get all site settings (Admin)
|
||||
// @Description Retrieve a list of all site settings including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.SiteSettings
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/site-settings [get]
|
||||
func (h *SiteSettingsHandler) AdminGetAllSiteSettings(c *gin.Context) {
|
||||
items, err := h.siteSettingsService.GetAllSiteSettings(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetSiteSettingsByID godoc
|
||||
// @Summary Get site settings by ID (Admin)
|
||||
// @Description Retrieve details of a specific site settings entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Site Settings ID"
|
||||
// @Success 200 {object} models.SiteSettings
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/site-settings/{id} [get]
|
||||
func (h *SiteSettingsHandler) AdminGetSiteSettingsByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.siteSettingsService.GetSiteSettingsByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateSiteSettings godoc
|
||||
// @Summary Create site settings (Admin)
|
||||
// @Description Create a new site settings entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body map[string]bool true "Site Settings Request"
|
||||
// @Success 201 {object} models.SiteSettings
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/site-settings [post]
|
||||
func (h *SiteSettingsHandler) CreateSiteSettings(c *gin.Context) {
|
||||
var req struct {
|
||||
IsActive *bool `json:"is_active"`
|
||||
SiteActive *bool `json:"site_active"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := true
|
||||
siteActive := true
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
if req.SiteActive != nil {
|
||||
siteActive = *req.SiteActive
|
||||
}
|
||||
|
||||
item, err := h.siteSettingsService.CreateSiteSettings(isActive, siteActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateSiteSettings godoc
|
||||
// @Summary Update site settings (Admin)
|
||||
// @Description Update an existing site settings entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Site Settings ID"
|
||||
// @Param request body map[string]bool true "Site Settings Request"
|
||||
// @Success 200 {object} models.SiteSettings
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/site-settings/{id} [put]
|
||||
func (h *SiteSettingsHandler) UpdateSiteSettings(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req struct {
|
||||
IsActive *bool `json:"is_active"`
|
||||
SiteActive *bool `json:"site_active"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
item, err := h.siteSettingsService.UpdateSiteSettings(id, req.IsActive, req.SiteActive)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "site settings not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteSiteSettings godoc
|
||||
// @Summary Delete site settings (Admin)
|
||||
// @Description Delete a site settings entry by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Site Settings ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/site-settings/{id} [delete]
|
||||
func (h *SiteSettingsHandler) DeleteSiteSettings(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.siteSettingsService.DeleteSiteSettings(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Site settings deleted successfully"})
|
||||
}
|
||||
217
api/handlers/skill_handler.go
Normal file
217
api/handlers/skill_handler.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SkillHandler struct {
|
||||
skillService *services.SkillService
|
||||
}
|
||||
|
||||
func NewSkillHandler(skillService *services.SkillService) *SkillHandler {
|
||||
return &SkillHandler{skillService: skillService}
|
||||
}
|
||||
|
||||
type CreateSkillRequest struct {
|
||||
Title string `json:"title"`
|
||||
Degree int `json:"degree"`
|
||||
ResumeID string `json:"resume_id"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UpdateSkillRequest struct {
|
||||
Title *string `json:"title"`
|
||||
Degree *int `json:"degree"`
|
||||
ResumeID *string `json:"resume_id"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// GetAllSkills godoc
|
||||
// @Summary Get all active skills
|
||||
// @Description Retrieve a list of active skill entries
|
||||
// @Tags resume
|
||||
// @Produce json
|
||||
// @Param resume_id query string false "Resume ID"
|
||||
// @Success 200 {array} models.Skill
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /skills [get]
|
||||
func (h *SkillHandler) GetAllSkills(c *gin.Context) {
|
||||
resumeID := strings.TrimSpace(c.Query("resume_id"))
|
||||
var resumeIDPtr *string
|
||||
if resumeID != "" {
|
||||
resumeIDPtr = &resumeID
|
||||
}
|
||||
items, err := h.skillService.GetAllSkills(resumeIDPtr, true)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetAllSkills godoc
|
||||
// @Summary Get all skills (Admin)
|
||||
// @Description Retrieve a list of all skill entries including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param resume_id query string false "Resume ID"
|
||||
// @Success 200 {array} models.Skill
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/skills [get]
|
||||
func (h *SkillHandler) AdminGetAllSkills(c *gin.Context) {
|
||||
resumeID := strings.TrimSpace(c.Query("resume_id"))
|
||||
var resumeIDPtr *string
|
||||
if resumeID != "" {
|
||||
resumeIDPtr = &resumeID
|
||||
}
|
||||
items, err := h.skillService.GetAllSkills(resumeIDPtr, false)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// AdminGetSkillByID godoc
|
||||
// @Summary Get a skill by ID (Admin)
|
||||
// @Description Retrieve details of a specific skill entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Skill ID"
|
||||
// @Success 200 {object} models.Skill
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/skills/{id} [get]
|
||||
func (h *SkillHandler) AdminGetSkillByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
item, err := h.skillService.GetSkillByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateSkill godoc
|
||||
// @Summary Create a new skill (Admin)
|
||||
// @Description Create a new skill entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreateSkillRequest true "Skill Request"
|
||||
// @Success 201 {object} models.Skill
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/skills [post]
|
||||
func (h *SkillHandler) CreateSkill(c *gin.Context) {
|
||||
var req CreateSkillRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
req.Title = strings.TrimSpace(req.Title)
|
||||
if req.Title == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := false
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
resumeUUID, parseErr := parseUUIDPtr(req.ResumeID)
|
||||
if parseErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume id"})
|
||||
return
|
||||
}
|
||||
item, err := h.skillService.CreateSkill(req.Title, req.Degree, resumeUUID, isActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateSkill godoc
|
||||
// @Summary Update a skill (Admin)
|
||||
// @Description Update an existing skill entry
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Skill ID"
|
||||
// @Param request body UpdateSkillRequest true "Skill Request"
|
||||
// @Success 200 {object} models.Skill
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/skills/{id} [put]
|
||||
func (h *SkillHandler) UpdateSkill(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req UpdateSkillRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title != nil {
|
||||
trimmed := strings.TrimSpace(*req.Title)
|
||||
req.Title = &trimmed
|
||||
}
|
||||
if req.ResumeID != nil {
|
||||
trimmed := strings.TrimSpace(*req.ResumeID)
|
||||
req.ResumeID = &trimmed
|
||||
}
|
||||
|
||||
var resumeUUIDPtr *uuid.UUID
|
||||
if req.ResumeID != nil {
|
||||
parsed, parseErr := parseUUIDPtr(*req.ResumeID)
|
||||
if parseErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume id"})
|
||||
return
|
||||
}
|
||||
resumeUUIDPtr = parsed
|
||||
}
|
||||
item, err := h.skillService.UpdateSkill(id, req.Title, req.Degree, resumeUUIDPtr, req.IsActive)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err.Error() == "skill not found" {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteSkill godoc
|
||||
// @Summary Delete a skill (Admin)
|
||||
// @Description Delete a skill by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Skill ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/skills/{id} [delete]
|
||||
func (h *SkillHandler) DeleteSkill(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.skillService.DeleteSkill(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Skill deleted successfully"})
|
||||
}
|
||||
167
api/handlers/tag_handler.go
Normal file
167
api/handlers/tag_handler.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type TagHandler struct {
|
||||
tagService *services.TagService
|
||||
}
|
||||
|
||||
func NewTagHandler(tagService *services.TagService) *TagHandler {
|
||||
return &TagHandler{tagService: tagService}
|
||||
}
|
||||
|
||||
type CreateTagRequest struct {
|
||||
Tag string `json:"tag" binding:"required"`
|
||||
IsActive *bool `json:"is_active"` // Pointer to allow false value
|
||||
}
|
||||
|
||||
type UpdateTagRequest struct {
|
||||
Tag string `json:"tag"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// GetAllTags godoc
|
||||
// @Summary Get all tags
|
||||
// @Description Retrieve a list of all tags. Public endpoint returns only active tags. Admin endpoint returns all.
|
||||
// @Tags tags
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Tag
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /tags [get]
|
||||
func (h *TagHandler) GetAllTags(c *gin.Context) {
|
||||
// Check if user is admin to decide whether to show all or only active
|
||||
// For simplicity in this public endpoint, we'll just return active ones
|
||||
// Admin specific endpoint can be added separately or controlled via query param + auth check
|
||||
|
||||
// Let's assume public access gets only active tags
|
||||
tags, err := h.tagService.GetAllTags(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, tags)
|
||||
}
|
||||
|
||||
// AdminGetAllTags godoc
|
||||
// @Summary Get all tags (Admin)
|
||||
// @Description Retrieve a list of all tags including inactive ones
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Tag
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /admin/tags [get]
|
||||
func (h *TagHandler) AdminGetAllTags(c *gin.Context) {
|
||||
tags, err := h.tagService.GetAllTags(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, tags)
|
||||
}
|
||||
|
||||
// CreateTag godoc
|
||||
// @Summary Create a new tag (Admin)
|
||||
// @Description Create a new tag
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreateTagRequest true "Tag Request"
|
||||
// @Success 201 {object} models.Tag
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /admin/tags [post]
|
||||
func (h *TagHandler) CreateTag(c *gin.Context) {
|
||||
var req CreateTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
isActive := true
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
tag, err := h.tagService.CreateTag(req.Tag, isActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, tag)
|
||||
}
|
||||
|
||||
// UpdateTag godoc
|
||||
// @Summary Update a tag (Admin)
|
||||
// @Description Update an existing tag
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Tag ID"
|
||||
// @Param request body UpdateTagRequest true "Tag Request"
|
||||
// @Success 200 {object} models.Tag
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/tags/{id} [put]
|
||||
func (h *TagHandler) UpdateTag(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req UpdateTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := h.tagService.UpdateTag(id, req.Tag, req.IsActive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tag)
|
||||
}
|
||||
|
||||
// DeleteTag godoc
|
||||
// @Summary Delete a tag (Admin)
|
||||
// @Description Delete a tag by ID
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Tag ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/tags/{id} [delete]
|
||||
func (h *TagHandler) DeleteTag(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.tagService.DeleteTag(id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Tag deleted successfully"})
|
||||
}
|
||||
|
||||
// GetTagByID godoc
|
||||
// @Summary Get a tag by ID (Admin)
|
||||
// @Description Retrieve details of a specific tag
|
||||
// @Tags admin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "Tag ID"
|
||||
// @Success 200 {object} models.Tag
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /admin/tags/{id} [get]
|
||||
func (h *TagHandler) GetTagByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
tag, err := h.tagService.GetTagByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, tag)
|
||||
}
|
||||
543
api/handlers/user_management_handler.go
Normal file
543
api/handlers/user_management_handler.go
Normal file
@@ -0,0 +1,543 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gauth-central/internal/database"
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UserManagementHandler struct {
|
||||
userService *services.UserManagementService
|
||||
}
|
||||
|
||||
func NewUserManagementHandler(userService *services.UserManagementService) *UserManagementHandler {
|
||||
return &UserManagementHandler{
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllUsers godoc
|
||||
// @Summary Get all users (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param limit query int false "Items per page" default(10)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users [get]
|
||||
func (h *UserManagementHandler) GetAllUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
users, total, err := h.userService.GetAllUsers(page, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": users,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"totalPages": (total + int64(limit) - 1) / int64(limit),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserByID godoc
|
||||
// @Summary Get user by ID (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} models.User
|
||||
// @Router /admin/users/{id} [get]
|
||||
func (h *UserManagementHandler) GetUserByID(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
user, err := h.userService.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// CreateUser godoc
|
||||
// @Summary Create new user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param email formData string true "Email"
|
||||
// @Param password formData string true "Password"
|
||||
// @Param user_name formData string true "Username"
|
||||
// @Param email_verified formData boolean false "Email verified"
|
||||
// @Param roles formData string false "Roles (comma separated: admin,user)"
|
||||
// @Param avatar formData file false "Avatar image"
|
||||
// @Success 201 {object} models.User
|
||||
// @Router /admin/users [post]
|
||||
func (h *UserManagementHandler) CreateUser(c *gin.Context) {
|
||||
// Parse multipart form
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse form data"})
|
||||
return
|
||||
}
|
||||
|
||||
email := c.PostForm("email")
|
||||
password := c.PostForm("password")
|
||||
userName := c.PostForm("user_name")
|
||||
emailVerified := c.PostForm("email_verified") == "true"
|
||||
rolesStr := c.PostForm("roles")
|
||||
|
||||
// Validate required fields
|
||||
if email == "" || password == "" || userName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "email, password, and user_name are required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse roles
|
||||
var roles []string
|
||||
if rolesStr != "" {
|
||||
roles = strings.Split(rolesStr, ",")
|
||||
// Trim spaces
|
||||
for i, role := range roles {
|
||||
roles[i] = strings.TrimSpace(role)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := h.userService.CreateUser(
|
||||
email,
|
||||
password,
|
||||
userName,
|
||||
emailVerified,
|
||||
roles,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Handle avatar upload if provided
|
||||
avatarFile, err := c.FormFile("avatar")
|
||||
if err == nil && avatarFile != nil {
|
||||
// Validate file size (max 5MB)
|
||||
if avatarFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Avatar file size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Use utils.SaveOptimizedImage
|
||||
// Default options (WebP, 800px width)
|
||||
avatarURL, err := utils.SaveOptimizedImage(avatarFile, "./uploads/avatars", user.ID.String(), nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update user avatar
|
||||
database.DB.Model(&user).Update("avatar", avatarURL)
|
||||
user.Avatar = avatarURL
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, user)
|
||||
}
|
||||
|
||||
// UpdateUser godoc
|
||||
// @Summary Update user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Param email formData string false "Email"
|
||||
// @Param password formData string false "Password"
|
||||
// @Param user_name formData string false "Username"
|
||||
// @Param email_verified formData boolean false "Email verified"
|
||||
// @Param roles formData string false "Roles (comma separated: admin,user)"
|
||||
// @Param avatar formData file false "Avatar image"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id} [put]
|
||||
func (h *UserManagementHandler) UpdateUser(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
// Try to parse as multipart form first
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
isMultipart := strings.Contains(contentType, "multipart/form-data")
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
var roles []string
|
||||
|
||||
if isMultipart {
|
||||
// Parse multipart form
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse form data"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get form values
|
||||
if email := c.PostForm("email"); email != "" {
|
||||
updates["email"] = email
|
||||
}
|
||||
if password := c.PostForm("password"); password != "" {
|
||||
updates["password"] = password
|
||||
}
|
||||
if userName := c.PostForm("user_name"); userName != "" {
|
||||
updates["user_name"] = userName
|
||||
}
|
||||
if emailVerified := c.PostForm("email_verified"); emailVerified != "" {
|
||||
updates["email_verified"] = emailVerified == "true"
|
||||
}
|
||||
if rolesStr := c.PostForm("roles"); rolesStr != "" {
|
||||
roles = strings.Split(rolesStr, ",")
|
||||
for i, role := range roles {
|
||||
roles[i] = strings.TrimSpace(role)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Parse as JSON
|
||||
var input struct {
|
||||
Email *string `json:"email"`
|
||||
Password *string `json:"password"`
|
||||
UserName *string `json:"user_name"`
|
||||
Avatar *string `json:"avatar"`
|
||||
EmailVerified *bool `json:"email_verified"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if input.Email != nil {
|
||||
updates["email"] = *input.Email
|
||||
}
|
||||
if input.Password != nil {
|
||||
updates["password"] = *input.Password
|
||||
}
|
||||
if input.UserName != nil {
|
||||
updates["user_name"] = *input.UserName
|
||||
}
|
||||
if input.Avatar != nil {
|
||||
updates["avatar"] = *input.Avatar
|
||||
}
|
||||
if input.EmailVerified != nil {
|
||||
updates["email_verified"] = *input.EmailVerified
|
||||
}
|
||||
if input.Roles != nil {
|
||||
roles = input.Roles
|
||||
}
|
||||
}
|
||||
|
||||
// Update basic user fields
|
||||
if len(updates) > 0 {
|
||||
if err := h.userService.UpdateUser(userID, updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Update roles if provided
|
||||
if len(roles) > 0 {
|
||||
if err := h.userService.AssignRoles(userID, roles); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update roles: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle avatar upload if multipart and file provided
|
||||
if isMultipart {
|
||||
avatarFile, err := c.FormFile("avatar")
|
||||
if err == nil && avatarFile != nil {
|
||||
// Validate file size (max 5MB)
|
||||
if avatarFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Avatar file size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user to check for old avatar
|
||||
var user models.User
|
||||
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete old avatar if exists and is local
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") {
|
||||
oldPath := "." + user.Avatar
|
||||
os.Remove(oldPath)
|
||||
}
|
||||
|
||||
// Use utils.SaveOptimizedImage
|
||||
avatarURL, err := utils.SaveOptimizedImage(avatarFile, "./uploads/avatars", userID, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update user avatar
|
||||
if err := database.DB.Model(&user).Update("avatar", avatarURL).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update avatar in database"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get updated user to return
|
||||
user, err := h.userService.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User updated successfully"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "User updated successfully",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteUser godoc
|
||||
// @Summary Delete user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param hard query boolean false "Hard delete (permanent)" default(false)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id} [delete]
|
||||
func (h *UserManagementHandler) DeleteUser(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
hardDelete := c.Query("hard") == "true"
|
||||
|
||||
// Prevent deleting self
|
||||
currentUserID := c.GetString("user_id")
|
||||
if userID == currentUserID {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete your own account"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userService.DeleteUser(userID, hardDelete); err != nil {
|
||||
if errors.Is(err, services.ErrUserNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"})
|
||||
return
|
||||
}
|
||||
|
||||
deleteType := "soft"
|
||||
if hardDelete {
|
||||
deleteType = "permanently"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User deleted " + deleteType + " successfully"})
|
||||
}
|
||||
|
||||
// AssignRoles godoc
|
||||
// @Summary Assign roles to user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Param roles body object true "Roles"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id}/roles [post]
|
||||
func (h *UserManagementHandler) AssignRoles(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
var input struct {
|
||||
Roles []string `json:"roles" binding:"required"` // ["admin", "user"]
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userService.AssignRoles(userID, input.Roles); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to assign roles: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Roles assigned successfully"})
|
||||
}
|
||||
|
||||
// RemoveRole godoc
|
||||
// @Summary Remove role from user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param role path string true "Role name"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id}/roles/{role} [delete]
|
||||
func (h *UserManagementHandler) RemoveRole(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
roleName := c.Param("role")
|
||||
|
||||
if err := h.userService.RemoveRole(userID, roleName); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove role"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Role removed successfully"})
|
||||
}
|
||||
|
||||
// SearchUsers godoc
|
||||
// @Summary Search users (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param q query string true "Search query"
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param limit query int false "Items per page" default(10)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/search [get]
|
||||
func (h *UserManagementHandler) SearchUsers(c *gin.Context) {
|
||||
query := c.Query("q")
|
||||
if query == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Search query required"})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
users, total, err := h.userService.SearchUsers(query, page, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search users"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": users,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"totalPages": (total + int64(limit) - 1) / int64(limit),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetDeletedUsers godoc
|
||||
// @Summary Get all soft deleted users (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param limit query int false "Items per page" default(10)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/deleted [get]
|
||||
func (h *UserManagementHandler) GetDeletedUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
users, total, err := h.userService.GetDeletedUsers(page, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch deleted users"})
|
||||
return
|
||||
}
|
||||
|
||||
// Transform users to include deleted_at field
|
||||
type DeletedUserResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserName string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deleted_at"`
|
||||
Roles []models.Role `json:"roles,omitempty"`
|
||||
SocialAccounts []models.SocialAccount `json:"social_accounts,omitempty"`
|
||||
}
|
||||
|
||||
deletedUsers := make([]DeletedUserResponse, len(users))
|
||||
for i, user := range users {
|
||||
var deletedAt *time.Time
|
||||
if user.DeletedAt.Valid {
|
||||
deletedAt = &user.DeletedAt.Time
|
||||
}
|
||||
|
||||
emailVerified := false
|
||||
if user.EmailVerified != nil {
|
||||
emailVerified = *user.EmailVerified
|
||||
}
|
||||
|
||||
deletedUsers[i] = DeletedUserResponse{
|
||||
ID: user.ID.String(),
|
||||
UserName: user.UserName,
|
||||
Email: user.Email,
|
||||
Avatar: user.Avatar,
|
||||
EmailVerified: emailVerified,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
DeletedAt: deletedAt,
|
||||
Roles: user.Roles,
|
||||
SocialAccounts: user.SocialAccounts,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": deletedUsers,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"totalPages": (total + int64(limit) - 1) / int64(limit),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// RestoreUser godoc
|
||||
// @Summary Restore a soft deleted user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id}/restore [post]
|
||||
func (h *UserManagementHandler) RestoreUser(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
if err := h.userService.RestoreUser(userID); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User restored successfully"})
|
||||
}
|
||||
Reference in New Issue
Block a user