first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:35:24 +03:00
commit bbbf76b184
592 changed files with 246870 additions and 0 deletions

View 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 = &degree
}
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
}

View 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)
}

View 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)
}

View 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,
},
})
}

View 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"})
}

View 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"})
}

View 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"})
}

View 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"})
}

View 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
}

View 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
}

View 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"})
}

View 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"})
}

View 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"})
}

View 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)
}

View 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"})
}

View 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
}

View 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)
}

View 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,
})
}

View 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"})
}

View 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"})
}

View 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"})
}

View 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"})
}

View 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 = &copyright
}
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"})
}

View 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"})
}

View 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
View 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)
}

View 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"})
}

View File

@@ -0,0 +1,49 @@
package middlewares
import (
"net/http"
"gauth-central/internal/database"
"gauth-central/internal/models"
"github.com/gin-gonic/gin"
)
// AdminMiddleware - Sadece admin rolündeki kullanıcıların erişimini sağlar
func AdminMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Get user_id from context (set by AuthMiddleware)
userID := c.GetString("user_id")
if userID == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
// Fetch user with roles
var user models.User
err := database.DB.Preload("Roles").Where("id = ?", userID).First(&user).Error
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
c.Abort()
return
}
// Check if user has admin role
hasAdminRole := false
for _, role := range user.Roles {
if role.Name == "admin" {
hasAdminRole = true
break
}
}
if !hasAdminRole {
c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
c.Abort()
return
}
c.Next()
}
}

View File

@@ -0,0 +1,47 @@
package middlewares
import (
"gauth-central/internal/services"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func AuthMiddleware(jwtService *services.JWTService) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
return
}
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
claims, err := jwtService.ValidateToken(tokenString)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token: " + err.Error()})
return
}
c.Set("user_id", claims.UserID)
c.Set("email", claims.Email)
c.Next()
}
}
// OptionalAuthMiddleware checks for a token but doesn't abort if it's missing or invalid.
// It sets user_id if a valid token is present.
func OptionalAuthMiddleware(jwtService *services.JWTService) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader != "" {
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
claims, err := jwtService.ValidateToken(tokenString)
if err == nil {
c.Set("user_id", claims.UserID)
c.Set("email", claims.Email)
}
}
c.Next()
}
}

View File

@@ -0,0 +1,57 @@
package middlewares
import (
"gauth-central/config"
"gauth-central/internal/services"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
// DynamicCorsMiddleware - Database'den okunan CORS ayarlarıyla çalışan middleware
func DynamicCorsMiddleware(settingsService *services.SettingsService) gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
// If no origin header, skip CORS
if origin == "" {
c.Next()
return
}
allowed, matchedEntry, matchedList, err := settingsService.CheckOrigin(origin)
if config.AppConfig != nil && config.AppConfig.CorsDebug {
log.Printf("cors_debug origin=%q allowed=%t matched_entry=%q matched_list=%q ip=%q", origin, allowed, matchedEntry, matchedList, c.ClientIP())
}
if err != nil {
// On error, log and deny
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "Failed to verify CORS policy",
})
return
}
if !allowed {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "Origin not allowed by CORS policy",
})
return
}
// Set CORS headers
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Max-Age", "86400") // 24 hours
// Handle preflight requests
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}

View File

@@ -0,0 +1,200 @@
package middlewares
import (
"fmt"
"net/http"
"strings"
"time"
"gauth-central/internal/services"
"gauth-central/pkg/utils"
"github.com/gin-gonic/gin"
)
// RateLimitMiddleware creates a rate limiting middleware
func RateLimitMiddleware(maxRequests int64, duration time.Duration) gin.HandlerFunc {
cacheService := services.NewCacheService()
settingsService := services.NewSettingsService()
return func(c *gin.Context) {
// Get client IP
clientIP := c.ClientIP()
path := c.Request.URL.Path
method := c.Request.Method
// Skip checks for localhost (hardcoded safety)
if clientIP == "::1" || clientIP == "127.0.0.1" || clientIP == "localhost" {
fmt.Printf("%s[LOCALHOST BYPASS]%s IP: %s accessed %s %s\n", utils.ColorCyan, utils.ColorReset, clientIP, method, path)
c.Next()
return
}
// 1. Check Blacklist from DB
blacklist, err := settingsService.GetActiveBlacklistOrigins()
if err == nil {
for _, blocked := range blacklist {
if blocked == clientIP || strings.Contains(blocked, clientIP) {
fmt.Printf("%s[BLACKLIST BLOCKED]%s IP: %s tried to access %s %s\n", utils.ColorRed, utils.ColorReset, clientIP, method, path)
c.JSON(http.StatusForbidden, gin.H{
"error": "Access denied. Your IP is blacklisted.",
})
c.Abort()
return
}
}
}
// 2. Check Whitelist from DB (Skip Rate Limit)
whitelist, err := settingsService.GetActiveWhitelistOrigins()
if err == nil {
for _, allowed := range whitelist {
if allowed == clientIP || strings.Contains(allowed, clientIP) {
fmt.Printf("%s[WHITELIST ALLOWED]%s IP: %s accessed %s %s (Rate Limit Skipped)\n", utils.ColorGreen, utils.ColorReset, clientIP, method, path)
c.Next()
return
}
}
}
key := clientIP
// Increment counter
count, err := cacheService.IncrementRateLimit(key, duration)
if err != nil {
// If Redis is down, allow the request but log error
fmt.Printf("%s[REDIS ERROR]%s Could not increment rate limit for %s: %v\n", utils.ColorRed, utils.ColorReset, clientIP, err)
c.Next()
return
}
remaining := maxRequests - count
if remaining < 0 {
remaining = 0
}
// Check if limit exceeded
if count > maxRequests {
fmt.Printf("%s[RATE LIMIT EXCEEDED]%s IP: %s - %s %s - Limit: %d\n", utils.ColorYellow, utils.ColorReset, clientIP, method, path, maxRequests)
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Too many requests. Please try again later.",
})
c.Abort()
return
}
// Log normal access with remaining limit
fmt.Printf("[Rate Limit] IP: %s - %s %s - Used: %d/%d - Remaining: %d\n", clientIP, method, path, count, maxRequests, remaining)
c.Next()
}
}
// DynamicRateLimitMiddleware - Database'den ayarları okuyan rate limit middleware
func DynamicRateLimitMiddleware(settingName string, settingsService *services.SettingsService) gin.HandlerFunc {
cacheService := services.NewCacheService()
return func(c *gin.Context) {
// Get client IP
clientIP := c.ClientIP()
path := c.Request.URL.Path
method := c.Request.Method
// Skip checks for localhost
if clientIP == "::1" || clientIP == "127.0.0.1" || clientIP == "localhost" {
fmt.Printf("%s[LOCALHOST BYPASS]%s IP: %s accessed %s %s\n", utils.ColorCyan, utils.ColorReset, clientIP, method, path)
c.Next()
return
}
// 1. Check Blacklist from DB
blacklist, err := settingsService.GetActiveBlacklistOrigins()
if err == nil {
for _, blocked := range blacklist {
if blocked == clientIP || strings.Contains(blocked, clientIP) {
fmt.Printf("%s[BLACKLIST BLOCKED]%s IP: %s tried to access %s %s\n", utils.ColorRed, utils.ColorReset, clientIP, method, path)
c.JSON(http.StatusForbidden, gin.H{
"error": "Access denied. Your IP is blacklisted.",
})
c.Abort()
return
}
}
}
// 2. Check Whitelist from DB (Skip Rate Limit)
whitelist, err := settingsService.GetActiveWhitelistOrigins()
if err == nil {
for _, allowed := range whitelist {
if allowed == clientIP || strings.Contains(allowed, clientIP) {
fmt.Printf("%s[WHITELIST ALLOWED]%s IP: %s accessed %s %s (Rate Limit Skipped)\n", utils.ColorGreen, utils.ColorReset, clientIP, method, path)
c.Next()
return
}
}
}
// Get rate limit settings from database/cache
setting, err := settingsService.GetRateLimitSettingByName(settingName)
if err != nil || setting == nil {
// If error or not found, use default and allow
c.Next()
return
}
// Check if setting is active
if !setting.IsActive {
c.Next()
return
}
key := settingName + ":" + clientIP
// Increment counter
duration := time.Duration(setting.WindowSeconds) * time.Second
count, err := cacheService.IncrementRateLimit(key, duration)
if err != nil {
// If Redis is down, allow the request
c.Next()
return
}
remaining := setting.MaxRequests - count
if remaining < 0 {
remaining = 0
}
// Check if limit exceeded
if count > setting.MaxRequests {
fmt.Printf("%s[RATE LIMIT EXCEEDED]%s IP: %s - %s %s - Limit: %d\n", utils.ColorYellow, utils.ColorReset, clientIP, method, path, setting.MaxRequests)
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Too many requests. Please try again later.",
"limit": setting.MaxRequests,
"window": setting.WindowSeconds,
"retry_after": setting.WindowSeconds,
})
c.Abort()
return
}
// Log normal access with remaining limit
fmt.Printf("[Rate Limit] IP: %s - %s %s - Used: %d/%d - Remaining: %d\n", clientIP, method, path, count, setting.MaxRequests, remaining)
c.Next()
}
}
// LoginRateLimitMiddleware limits login attempts per IP
func LoginRateLimitMiddleware() gin.HandlerFunc {
return RateLimitMiddleware(5, 1*time.Minute) // 5 login attempts per minute
}
// RegisterRateLimitMiddleware limits registration attempts per IP
func RegisterRateLimitMiddleware() gin.HandlerFunc {
return RateLimitMiddleware(3, 5*time.Minute) // 3 registration attempts per 5 minutes
}
// APIRateLimitMiddleware general API rate limiting
func APIRateLimitMiddleware() gin.HandlerFunc {
return RateLimitMiddleware(100, 1*time.Minute) // 100 requests per minute
}

464
api/routes/routes.go Normal file
View File

@@ -0,0 +1,464 @@
package routes
import (
"gauth-central/api/handlers"
"gauth-central/api/middlewares"
_ "gauth-central/docs" // docs import
"gauth-central/internal/services"
"net/http"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func SetupRoutes(r *gin.Engine) {
jwtService := services.NewJWTService()
authService := services.NewAuthService()
authHandler := handlers.NewAuthHandler(authService)
settingsService := services.NewSettingsService()
settingsHandler := handlers.NewSettingsHandler(settingsService)
userManagementService := services.NewUserManagementService()
userManagementHandler := handlers.NewUserManagementHandler(userManagementService)
avatarHandler := handlers.NewAvatarHandler()
profileHandler := handlers.NewProfileHandler()
contactService := services.NewContactService()
contactHandler := handlers.NewContactHandler(contactService)
tagService := services.NewTagService()
tagHandler := handlers.NewTagHandler(tagService)
postCategoryService := services.NewPostCategoryService()
postCategoryHandler := handlers.NewPostCategoryHandler(postCategoryService)
postTagService := services.NewPostTagService()
postTagHandler := handlers.NewPostTagHandler(postTagService)
postService := services.NewPostService()
postHandler := handlers.NewPostHandler(postService)
postCommentService := services.NewPostCommentService()
postCommentHandler := handlers.NewPostCommentHandler(postCommentService)
postCategoryViewService := services.NewPostCategoryViewService()
postCategoryViewHandler := handlers.NewPostCategoryViewHandler(postCategoryViewService)
homeService := services.NewHomeService()
homeHandler := handlers.NewHomeHandler(homeService)
aboutService := services.NewAboutService()
aboutHandler := handlers.NewAboutHandler(aboutService)
serviceService := services.NewServiceService()
serviceHandler := handlers.NewServiceHandler(serviceService)
serviceTitleService := services.NewServiceTitleService()
serviceTitleHandler := handlers.NewServiceTitleHandler(serviceTitleService)
siteInfoService := services.NewSiteInfoService()
siteInfoHandler := handlers.NewSiteInfoHandler(siteInfoService)
bannerService := services.NewBannerService()
bannerHandler := handlers.NewBannerHandler(bannerService)
siteSettingsService := services.NewSiteSettingsService()
siteSettingsHandler := handlers.NewSiteSettingsHandler(siteSettingsService)
resumeService := services.NewResumeService()
resumeHandler := handlers.NewResumeHandler(resumeService)
educationService := services.NewEducationService()
educationHandler := handlers.NewEducationHandler(educationService)
experienceService := services.NewExperienceService()
experienceHandler := handlers.NewExperienceHandler(experienceService)
skillService := services.NewSkillService()
skillHandler := handlers.NewSkillHandler(skillService)
knowledgeService := services.NewKnowledgeService()
knowledgeHandler := handlers.NewKnowledgeHandler(knowledgeService)
mainMenuService := services.NewMainMenuService()
mainMenuHandler := handlers.NewMainMenuHandler(mainMenuService)
// Serve static files (uploaded avatars)
r.Static("/uploads", "./uploads")
// Homepage
r.LoadHTMLGlob("web/*")
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
// Swagger route moved outside of v1 group to be accessible at /docs/index.html
r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
v1 := r.Group("/v1")
v1.Use(middlewares.APIRateLimitMiddleware()) // General API rate limiting
{
auth := v1.Group("/auth")
{
auth.POST("/register", middlewares.RegisterRateLimitMiddleware(), authHandler.Register)
auth.POST("/login", middlewares.LoginRateLimitMiddleware(), authHandler.Login)
auth.GET("/verify-email", authHandler.VerifyEmail)
auth.GET("/:provider", authHandler.BeginAuth)
auth.GET("/:provider/callback", authHandler.Callback)
auth.POST("/refresh", authHandler.Refresh)
// Protected routes
protected := auth.Group("/")
protected.Use(middlewares.AuthMiddleware(jwtService))
{
protected.GET("/me", authHandler.Me)
protected.GET("/validate", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Token is valid",
"user_id": c.GetString("user_id"),
"email": c.GetString("email"),
})
})
}
}
// Contact endpoint (Public but can optionally use auth)
v1.POST("/contact", middlewares.OptionalAuthMiddleware(jwtService), contactHandler.CreateContact)
// Public Tags Endpoint (Only active tags)
v1.GET("/tags", tagHandler.GetAllTags)
// Public Post Categories
v1.GET("/post-categories", postCategoryHandler.GetAllPostCategories)
v1.GET("/post-categories/:slug", postCategoryHandler.GetPostCategoryBySlug)
v1.POST("/post-categories/:id/views", postCategoryViewHandler.TrackPostCategoryView)
// Public Post Tags
v1.GET("/post-tags", postTagHandler.GetAllPostTags)
// Public Posts
v1.GET("/posts", postHandler.GetAllPosts)
v1.GET("/posts/slug/:slug", postHandler.GetPostBySlug)
v1.GET("/posts/:id/comments", postCommentHandler.GetPostCommentsByPostID)
// Public About Endpoints (Only active about entries)
v1.GET("/about", aboutHandler.GetAllAbout)
v1.GET("/about/active", aboutHandler.GetActiveAbout)
// Public Services Endpoints (Only active services)
v1.GET("/services", serviceHandler.GetAllServices)
v1.GET("/services/:slug", serviceHandler.GetServiceBySlug)
// Public Service Titles Endpoints (Only active service titles)
v1.GET("/service-titles", serviceTitleHandler.GetAllServiceTitles)
v1.GET("/service-titles/active", serviceTitleHandler.GetActiveServiceTitle)
// Public Main Menu Endpoints (Only active entries)
v1.GET("/main-menu", mainMenuHandler.GetAllMainMenus)
v1.GET("/main-menu/active", mainMenuHandler.GetActiveMainMenu)
// Public Site Info Endpoints (Only active entries)
v1.GET("/site-info", siteInfoHandler.GetAllSiteInfos)
v1.GET("/site-info/active", siteInfoHandler.GetActiveSiteInfo)
// Public Banner Endpoints (Only active entries)
v1.GET("/banners", bannerHandler.GetAllBanners)
v1.GET("/banners/active", bannerHandler.GetActiveBanner)
// Public Site Settings Endpoints (Only active entries)
v1.GET("/site-settings", siteSettingsHandler.GetAllSiteSettings)
v1.GET("/site-settings/active", siteSettingsHandler.GetActiveSiteSettings)
// Public Homes Endpoints (Only active homes)
v1.GET("/homes", homeHandler.GetAllHomes)
v1.GET("/homes/:slug", homeHandler.GetHomeBySlug)
// Public Resume Endpoints
v1.GET("/resumes", resumeHandler.GetAllResumes)
v1.GET("/resumes/active", resumeHandler.GetActiveResume)
v1.GET("/educations", educationHandler.GetAllEducations)
v1.GET("/experiences", experienceHandler.GetAllExperiences)
v1.GET("/skills", skillHandler.GetAllSkills)
v1.GET("/knowledges", knowledgeHandler.GetAllKnowledges)
// User endpoints
user := v1.Group("/user")
user.Use(middlewares.AuthMiddleware(jwtService))
{
// Avatar management
user.POST("/avatar", avatarHandler.UploadAvatar)
user.DELETE("/avatar", avatarHandler.DeleteAvatar)
}
// Post comment creation (Auth required)
postAuth := v1.Group("/posts")
postAuth.Use(middlewares.AuthMiddleware(jwtService))
{
postAuth.POST("/:id/comments", postCommentHandler.CreatePostComment)
}
// Profile endpoints
profile := v1.Group("/profile")
profile.Use(middlewares.AuthMiddleware(jwtService))
{
profile.GET("", profileHandler.GetProfile)
profile.PUT("", profileHandler.UpdateProfile)
profile.PUT("/password", profileHandler.ChangePassword)
profile.PUT("/email", profileHandler.ChangeEmail)
}
// Settings endpoints (Admin only)
settings := v1.Group("/settings")
settings.Use(middlewares.AuthMiddleware(jwtService))
settings.Use(middlewares.AdminMiddleware())
{
// CORS Whitelist
corsWhitelist := settings.Group("/cors/whitelist")
{
corsWhitelist.GET("", settingsHandler.GetAllWhitelist)
corsWhitelist.POST("", settingsHandler.CreateWhitelist)
corsWhitelist.PUT("/:id", settingsHandler.UpdateWhitelist)
corsWhitelist.DELETE("/:id", settingsHandler.DeleteWhitelist)
}
// CORS Blacklist
corsBlacklist := settings.Group("/cors/blacklist")
{
corsBlacklist.GET("", settingsHandler.GetAllBlacklist)
corsBlacklist.POST("", settingsHandler.CreateBlacklist)
corsBlacklist.PUT("/:id", settingsHandler.UpdateBlacklist)
corsBlacklist.DELETE("/:id", settingsHandler.DeleteBlacklist)
}
// Rate Limit Settings
rateLimit := settings.Group("/ratelimit")
{
rateLimit.GET("", settingsHandler.GetAllRateLimits)
rateLimit.PUT("/:id", settingsHandler.UpdateRateLimit)
}
// CORS Cache
settings.POST("/cors/cache/invalidate", settingsHandler.InvalidateCorsCache)
}
// Admin - User Management
admin := v1.Group("/admin")
admin.Use(middlewares.AuthMiddleware(jwtService))
admin.Use(middlewares.AdminMiddleware())
{
users := admin.Group("/users")
{
users.GET("/search", userManagementHandler.SearchUsers)
users.GET("/deleted", userManagementHandler.GetDeletedUsers) // Yeni: Silinen kullanıcılar
users.GET("", userManagementHandler.GetAllUsers)
users.POST("", userManagementHandler.CreateUser)
users.GET("/:id", userManagementHandler.GetUserByID)
users.PUT("/:id", userManagementHandler.UpdateUser)
users.DELETE("/:id", userManagementHandler.DeleteUser)
users.POST("/:id/roles", userManagementHandler.AssignRoles)
users.DELETE("/:id/roles/:role", userManagementHandler.RemoveRole)
users.POST("/:id/restore", userManagementHandler.RestoreUser) // Yeni: Kullanıcıyı restore et
// Avatar management for users (Admin)
users.POST("/:id/avatar", avatarHandler.AdminUploadAvatar)
}
// Admin - Home Management
homes := admin.Group("/homes")
{
homes.GET("", homeHandler.AdminGetAllHomes)
homes.POST("", homeHandler.CreateHome)
homes.GET("/:id", homeHandler.AdminGetHomeByID)
homes.PUT("/:id", homeHandler.UpdateHome)
homes.DELETE("/:id", homeHandler.DeleteHome)
homes.POST("/:id/image", homeHandler.AdminUploadHomeImage)
}
// Admin - Post Categories
postCategories := admin.Group("/post-categories")
{
postCategories.GET("", postCategoryHandler.AdminGetAllPostCategories)
postCategories.POST("", postCategoryHandler.CreatePostCategory)
postCategories.GET("/:id", postCategoryHandler.AdminGetPostCategoryByID)
postCategories.PUT("/:id", postCategoryHandler.UpdatePostCategory)
postCategories.DELETE("/:id", postCategoryHandler.DeletePostCategory)
}
// Admin - Post Tags
postTags := admin.Group("/post-tags")
{
postTags.GET("", postTagHandler.AdminGetAllPostTags)
postTags.POST("", postTagHandler.CreatePostTag)
postTags.GET("/:id", postTagHandler.GetPostTagByID)
postTags.PUT("/:id", postTagHandler.UpdatePostTag)
postTags.DELETE("/:id", postTagHandler.DeletePostTag)
}
// Admin - Posts
posts := admin.Group("/posts")
{
posts.GET("", postHandler.AdminGetAllPosts)
posts.POST("", postHandler.CreatePost)
posts.GET("/:id", postHandler.AdminGetPostByID)
posts.PUT("/:id", postHandler.UpdatePost)
posts.DELETE("/:id", postHandler.DeletePost)
}
// Admin - Post Comments
postComments := admin.Group("/post-comments")
{
postComments.GET("", postCommentHandler.AdminGetAllPostComments)
postComments.GET("/:id", postCommentHandler.AdminGetPostCommentByID)
postComments.PUT("/:id", postCommentHandler.AdminUpdatePostComment)
postComments.DELETE("/:id", postCommentHandler.AdminDeletePostComment)
}
// Admin - Post Category Views
postCategoryViews := admin.Group("/post-category-views")
{
postCategoryViews.GET("", postCategoryViewHandler.AdminGetPostCategoryViews)
}
// Admin - About Management
about := admin.Group("/about")
{
about.GET("", aboutHandler.AdminGetAllAbout)
about.POST("", aboutHandler.CreateAbout)
about.GET("/:id", aboutHandler.AdminGetAboutByID)
about.PUT("/:id", aboutHandler.UpdateAbout)
about.DELETE("/:id", aboutHandler.DeleteAbout)
}
// Admin - Service Management
servicesGroup := admin.Group("/services")
{
servicesGroup.GET("", serviceHandler.AdminGetAllServices)
servicesGroup.POST("", serviceHandler.CreateService)
servicesGroup.GET("/:id", serviceHandler.AdminGetServiceByID)
servicesGroup.PUT("/:id", serviceHandler.UpdateService)
servicesGroup.DELETE("/:id", serviceHandler.DeleteService)
}
// Admin - Service Title Management
serviceTitles := admin.Group("/service-titles")
{
serviceTitles.GET("", serviceTitleHandler.AdminGetAllServiceTitles)
serviceTitles.POST("", serviceTitleHandler.CreateServiceTitle)
serviceTitles.GET("/:id", serviceTitleHandler.AdminGetServiceTitleByID)
serviceTitles.PUT("/:id", serviceTitleHandler.UpdateServiceTitle)
serviceTitles.DELETE("/:id", serviceTitleHandler.DeleteServiceTitle)
}
// Admin - Site Info Management
siteInfo := admin.Group("/site-info")
{
siteInfo.GET("", siteInfoHandler.AdminGetAllSiteInfos)
siteInfo.POST("", siteInfoHandler.CreateSiteInfo)
siteInfo.GET("/:id", siteInfoHandler.AdminGetSiteInfoByID)
siteInfo.PUT("/:id", siteInfoHandler.UpdateSiteInfo)
siteInfo.DELETE("/:id", siteInfoHandler.DeleteSiteInfo)
}
// Admin - Banner Management
banners := admin.Group("/banners")
{
banners.GET("", bannerHandler.AdminGetAllBanners)
banners.POST("", bannerHandler.CreateBanner)
banners.GET("/:id", bannerHandler.AdminGetBannerByID)
banners.PUT("/:id", bannerHandler.UpdateBanner)
banners.DELETE("/:id", bannerHandler.DeleteBanner)
}
// Admin - Site Settings Management
siteSettings := admin.Group("/site-settings")
{
siteSettings.GET("", siteSettingsHandler.AdminGetAllSiteSettings)
siteSettings.POST("", siteSettingsHandler.CreateSiteSettings)
siteSettings.GET("/:id", siteSettingsHandler.AdminGetSiteSettingsByID)
siteSettings.PUT("/:id", siteSettingsHandler.UpdateSiteSettings)
siteSettings.DELETE("/:id", siteSettingsHandler.DeleteSiteSettings)
}
// Admin - Resume Management
resumes := admin.Group("/resumes")
{
resumes.GET("", resumeHandler.AdminGetAllResumes)
resumes.POST("", resumeHandler.CreateResume)
resumes.GET("/:id", resumeHandler.AdminGetResumeByID)
resumes.PUT("/:id", resumeHandler.UpdateResume)
resumes.DELETE("/:id", resumeHandler.DeleteResume)
}
// Admin - Education Management
educations := admin.Group("/educations")
{
educations.GET("", educationHandler.AdminGetAllEducations)
educations.POST("", educationHandler.CreateEducation)
educations.GET("/:id", educationHandler.AdminGetEducationByID)
educations.PUT("/:id", educationHandler.UpdateEducation)
educations.DELETE("/:id", educationHandler.DeleteEducation)
}
// Admin - Experience Management
experiences := admin.Group("/experiences")
{
experiences.GET("", experienceHandler.AdminGetAllExperiences)
experiences.POST("", experienceHandler.CreateExperience)
experiences.GET("/:id", experienceHandler.AdminGetExperienceByID)
experiences.PUT("/:id", experienceHandler.UpdateExperience)
experiences.DELETE("/:id", experienceHandler.DeleteExperience)
}
// Admin - Skill Management
skills := admin.Group("/skills")
{
skills.GET("", skillHandler.AdminGetAllSkills)
skills.POST("", skillHandler.CreateSkill)
skills.GET("/:id", skillHandler.AdminGetSkillByID)
skills.PUT("/:id", skillHandler.UpdateSkill)
skills.DELETE("/:id", skillHandler.DeleteSkill)
}
// Admin - Knowledge Management
knowledges := admin.Group("/knowledges")
{
knowledges.GET("", knowledgeHandler.AdminGetAllKnowledges)
knowledges.POST("", knowledgeHandler.CreateKnowledge)
knowledges.GET("/:id", knowledgeHandler.AdminGetKnowledgeByID)
knowledges.PUT("/:id", knowledgeHandler.UpdateKnowledge)
knowledges.DELETE("/:id", knowledgeHandler.DeleteKnowledge)
}
// Admin - Main Menu Management
mainMenu := admin.Group("/main-menu")
{
mainMenu.GET("", mainMenuHandler.AdminGetAllMainMenus)
mainMenu.POST("", mainMenuHandler.CreateMainMenu)
mainMenu.GET("/:id", mainMenuHandler.AdminGetMainMenuByID)
mainMenu.PUT("/:id", mainMenuHandler.UpdateMainMenu)
mainMenu.DELETE("/:id", mainMenuHandler.DeleteMainMenu)
}
// Admin - Contact Management
contacts := admin.Group("/contacts")
{
contacts.GET("", contactHandler.GetAllContacts)
contacts.GET("/:id", contactHandler.GetContactByID)
contacts.DELETE("/:id", contactHandler.DeleteContact)
}
// Admin - Tag Management
tags := admin.Group("/tags")
{
tags.GET("", tagHandler.AdminGetAllTags)
tags.POST("", tagHandler.CreateTag)
tags.GET("/:id", tagHandler.GetTagByID)
tags.PUT("/:id", tagHandler.UpdateTag)
tags.DELETE("/:id", tagHandler.DeleteTag)
}
}
}
}