first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:43:40 +03:00
commit f34e54c5a5
100 changed files with 27342 additions and 0 deletions

View File

@@ -0,0 +1,361 @@
package handlers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"gobeyhan/app/account/services"
settingsServices "gobeyhan/app/settings/services"
"gobeyhan/config"
"gobeyhan/database/models"
"github.com/gin-gonic/gin"
)
type AuthHandler struct {
userService *services.UserService
jwtService *settingsServices.JWTService
}
func NewAuthHandler(userService *services.UserService, jwtService *settingsServices.JWTService) *AuthHandler {
return &AuthHandler{
userService: userService,
jwtService: jwtService,
}
}
// Register godoc
// @Summary Register a new user
// @Description Create a new user account with email and password
// @Tags auth
// @Accept json
// @Produce json
// @Param request body object{email=string,password=string,username=string} true "Registration data"
// @Success 201 {object} object{token=string,user=models.User}
// @Failure 400 {object} object{error=string}
// @Router /api/v1/auth/register [post]
func (h *AuthHandler) Register(c *gin.Context) {
var input struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Username string `json:"username" binding:"required,min=3"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Create user (password will be hashed by UserService)
user := &models.User{
Email: input.Email,
UserName: input.Username,
}
// Password is passed separately to be hashed
if err := h.userService.CreateUser(user, input.Password); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Assign default role
if err := h.userService.AssignDefaultRole(user.ID); err != nil {
// Log error but don't fail registration
// log.Printf("Failed to assign default role: %v", err)
}
// Generate JWT tokens
accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
return
}
// Return tokens and user (without password)
user.Password = ""
c.JSON(http.StatusCreated, gin.H{
"access_token": accessToken,
"refresh_token": refreshToken,
"user": user,
})
}
// Login godoc
// @Summary Login user
// @Description Login with email and password
// @Tags auth
// @Accept json
// @Produce json
// @Param request body object{email=string,password=string} true "Login credentials"
// @Success 200 {object} object{token=string,user=models.User}
// @Failure 400 {object} object{error=string}
// @Failure 401 {object} object{error=string}
// @Router /api/v1/auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
var input struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
TurnstileToken string `json:"turnstile_token"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Verify Turnstile
if !verifyTurnstile(input.TurnstileToken) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Turnstile doğrulaması başarısız"})
return
}
// Get user by email
user, err := h.userService.GetUserByEmail(input.Email)
if err != nil || user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
// Verify password
if !h.userService.VerifyPassword(user.Password, input.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
// Generate JWT tokens
accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
return
}
// Set refresh token as HttpOnly cookie (secure, XSS-safe)
cookie := &http.Cookie{
Name: "refresh_token",
Value: refreshToken,
Path: "/",
Domain: "localhost", // Explicitly set for local dev
MaxAge: 7 * 24 * 60 * 60, // 7 days
Secure: false, // Set true for HTTPS in production
HttpOnly: true, // Cannot be accessed by JavaScript
SameSite: http.SameSiteLaxMode, // Lax is better for local dev
}
http.SetCookie(c.Writer, cookie)
fmt.Printf("[DEBUG] Login - Set-Cookie Raw: %s\n", cookie.String())
// Return tokens and user (without password)
user.Password = ""
c.JSON(http.StatusOK, gin.H{
"access_token": accessToken,
"refresh_token": refreshToken, // Also in response for fallback
"user": user,
})
}
// GetCurrentUser godoc
// @Summary Get current user
// @Description Get current authenticated user information
// @Tags auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} models.User
// @Failure 401 {object} object{error=string}
// @Router /api/v1/auth/me [get]
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
// Get user ID from context (set by auth middleware)
userIDStr, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
// Convert to uint64
var userID uint64
switch v := userIDStr.(type) {
case string:
parsed, _ := strconv.ParseUint(v, 10, 64)
userID = parsed
case uint64:
userID = v
case int:
userID = uint64(v)
case float64:
userID = uint64(v)
}
// Get user from database
user, err := h.userService.GetUserByID(userID)
if err != nil || user == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
// Return user (without password)
user.Password = ""
c.JSON(http.StatusOK, user)
}
// Logout godoc
// @Summary Logout user
// @Description Logout (client-side token removal)
// @Tags auth
// @Accept json
// @Produce json
// @Success 200 {object} object{message=string}
// @Router /api/v1/auth/logout [post]
func (h *AuthHandler) Logout(c *gin.Context) {
// Clear refresh token cookie
cookie := &http.Cookie{
Name: "refresh_token",
Value: "",
Path: "/",
Domain: "localhost",
MaxAge: -1, // Delete cookie
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(c.Writer, cookie)
fmt.Printf("[DEBUG] Logout - Set-Cookie Raw: %s\n", cookie.String())
// For JWT, logout is typically handled client-side
// Server can implement token blacklisting if needed
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
}
// RefreshToken godoc
// @Summary Refresh access token
// @Description Get a new access token using refresh token from HttpOnly cookie
// @Tags auth
// @Accept json
// @Produce json
// @Success 200 {object} object{access_token=string,refresh_token=string}
// @Failure 401 {object} object{error=string}
// @Router /api/v1/auth/refresh [post]
func (h *AuthHandler) RefreshToken(c *gin.Context) {
// Get refresh token from HttpOnly cookie
refreshToken, err := c.Cookie("refresh_token")
if err != nil {
fmt.Printf("[DEBUG] RefreshToken - Cookie Error: %v\n", err)
} else {
fmt.Printf("[DEBUG] RefreshToken - Cookie Found: %s...\n", refreshToken[:10])
}
if err != nil || refreshToken == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Refresh token not found"})
return
}
// Validate refresh token and get user ID
claims, err := h.jwtService.ValidateToken(refreshToken)
if err != nil {
fmt.Printf("[DEBUG] RefreshToken - ValidateToken Error: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired refresh token"})
return
}
// Get user ID from claims
userID, err := claims.GetSubject()
if err != nil {
fmt.Printf("[DEBUG] RefreshToken - GetSubject Error: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
return
}
// Parse user ID to uint64
// Sscan bazen boşluk vs. yüzünden hata verebilir, strconv daha güvenli
parsedUint, err := strconv.ParseUint(userID, 10, 64)
if err != nil {
fmt.Printf("[DEBUG] RefreshToken - ParseUint Error: %v (userID=%s)\n", err, userID)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID format"})
return
}
uid := parsedUint
// Get user from database
user, err := h.userService.GetUserByID(uid)
if err != nil {
fmt.Printf("[DEBUG] RefreshToken - GetUserByID FAILED: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Database error"})
return
}
if user == nil {
fmt.Printf("[DEBUG] RefreshToken - User not found in DB for ID: %d\n", uid)
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
return
}
fmt.Printf("[DEBUG] RefreshToken - User found: %s\n", user.Email)
// Generate new token pair
newAccessToken, newRefreshToken, err := h.jwtService.GenerateTokenPair(*user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
return
}
// Update refresh token cookie (token rotation)
cookie := &http.Cookie{
Name: "refresh_token",
Value: newRefreshToken,
Path: "/",
Domain: "localhost",
MaxAge: 7 * 24 * 60 * 60, // 7 days
Secure: false,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(c.Writer, cookie)
// Return new access token and user info
user.Password = "" // Ensure password is not sent
c.JSON(http.StatusOK, gin.H{
"access_token": newAccessToken,
"refresh_token": newRefreshToken,
"user": user, // Critical for frontend session restore
})
}
// Helper: Verify Turnstile Token
func verifyTurnstile(token string) bool {
secret := config.AppConfig.TurnstileSecretKey
if secret == "" {
fmt.Println("[WARNING] Turnstile Secret Key not configured, skipping validation")
return true // Skip validation if not configured
}
if token == "" {
// If secret is configured, token is mandatory
return false
}
formData := url.Values{
"secret": {secret},
"response": {token},
}
resp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", formData)
if err != nil {
fmt.Printf("[ERROR] Turnstile request failed: %v\n", err)
return false
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("[ERROR] Failed to read Turnstile response: %v\n", err)
return false
}
var result struct {
Success bool `json:"success"`
}
if err := json.Unmarshal(body, &result); err != nil {
fmt.Printf("[ERROR] Failed to parse Turnstile response: %v\n", err)
return false
}
return result.Success
}

View File

@@ -0,0 +1,297 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"gobeyhan/app/account/services"
settingsServices "gobeyhan/app/settings/services"
"gobeyhan/config"
"gobeyhan/database/models"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
"golang.org/x/oauth2/google"
)
type OAuthHandler struct {
userService *services.UserService
socialAccountService *services.SocialAccountService
jwtService *settingsServices.JWTService
googleOAuthConfig *oauth2.Config
githubOAuthConfig *oauth2.Config
}
func NewOAuthHandler(
userService *services.UserService,
socialAccountService *services.SocialAccountService,
jwtService *settingsServices.JWTService,
) *OAuthHandler {
// Google OAuth config
googleConfig := &oauth2.Config{
ClientID: config.AppConfig.GoogleClientID,
ClientSecret: config.AppConfig.GoogleClientSecret,
RedirectURL: config.AppConfig.GoogleRedirectURL,
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
},
Endpoint: google.Endpoint,
}
// GitHub OAuth config
githubConfig := &oauth2.Config{
ClientID: config.AppConfig.GithubClientID,
ClientSecret: config.AppConfig.GithubClientSecret,
RedirectURL: config.AppConfig.GithubRedirectURL,
Scopes: []string{"user:email"},
Endpoint: github.Endpoint,
}
return &OAuthHandler{
userService: userService,
socialAccountService: socialAccountService,
jwtService: jwtService,
googleOAuthConfig: googleConfig,
githubOAuthConfig: githubConfig,
}
}
// GoogleLogin godoc
// @Summary Google OAuth login
// @Description Redirect to Google OAuth
// @Tags auth,oauth
// @Produce json
// @Router /api/v1/auth/google [get]
func (h *OAuthHandler) GoogleLogin(c *gin.Context) {
url := h.googleOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
c.Redirect(http.StatusTemporaryRedirect, url)
}
// GoogleCallback godoc
// @Summary Google OAuth callback
// @Description Handle Google OAuth callback
// @Tags auth,oauth
// @Produce json
// @Param code query string true "Authorization code"
// @Success 200 {object} object{token=string,user=models.User}
// @Failure 400 {object} object{error=string}
// @Router /api/v1/auth/google/callback [get]
func (h *OAuthHandler) GoogleCallback(c *gin.Context) {
code := c.Query("code")
if code == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Code not found"})
return
}
// Exchange code for token
token, err := h.googleOAuthConfig.Exchange(context.Background(), code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
return
}
// Get user info from Google
client := h.googleOAuthConfig.Client(context.Background(), token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"})
return
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var googleUser struct {
ID string `json:"id"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
Name string `json:"name"`
Picture string `json:"picture"`
}
json.Unmarshal(data, &googleUser)
// Find or create user
user, accessToken, refreshToken, err := h.findOrCreateOAuthUser(
googleUser.Email,
googleUser.Name,
"google",
googleUser.ID,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": accessToken,
"refresh_token": refreshToken,
"user": user,
})
}
// GithubLogin godoc
// @Summary GitHub OAuth login
// @Description Redirect to GitHub OAuth
// @Tags auth,oauth
// @Produce json
// @Router /api/v1/auth/github [get]
func (h *OAuthHandler) GithubLogin(c *gin.Context) {
url := h.githubOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
c.Redirect(http.StatusTemporaryRedirect, url)
}
// GithubCallback godoc
// @Summary GitHub OAuth callback
// @Description Handle GitHub OAuth callback
// @Tags auth,oauth
// @Produce json
// @Param code query string true "Authorization code"
// @Success 200 {object} object{token=string,user=models.User}
// @Failure 400 {object} object{error=string}
// @Router /api/v1/auth/github/callback [get]
func (h *OAuthHandler) GithubCallback(c *gin.Context) {
code := c.Query("code")
if code == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Code not found"})
return
}
// Exchange code for token
token, err := h.githubOAuthConfig.Exchange(context.Background(), code)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
return
}
// Get user info from GitHub
client := h.githubOAuthConfig.Client(context.Background(), token)
resp, err := client.Get("https://api.github.com/user")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"})
return
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var githubUser struct {
ID int `json:"id"`
Login string `json:"login"`
Email string `json:"email"`
Name string `json:"name"`
}
json.Unmarshal(data, &githubUser)
// If email is not public, fetch it separately
if githubUser.Email == "" {
emailResp, _ := client.Get("https://api.github.com/user/emails")
if emailResp != nil {
defer emailResp.Body.Close()
emailData, _ := io.ReadAll(emailResp.Body)
var emails []struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
json.Unmarshal(emailData, &emails)
for _, e := range emails {
if e.Primary && e.Verified {
githubUser.Email = e.Email
break
}
}
}
}
username := githubUser.Name
if username == "" {
username = githubUser.Login
}
// Find or create user
user, accessToken, refreshToken, err := h.findOrCreateOAuthUser(
githubUser.Email,
username,
"github",
strconv.Itoa(githubUser.ID),
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": accessToken,
"refresh_token": refreshToken,
"user": user,
})
}
// findOrCreateOAuthUser finds existing user or creates new one for OAuth
func (h *OAuthHandler) findOrCreateOAuthUser(
email, username, provider, providerUserID string,
) (*models.User, string, string, error) {
// Try to find existing user by email
user, err := h.userService.GetUserByEmail(email)
if err != nil {
return nil, "", "", err
}
// If user doesn't exist, create new one
if user == nil {
user = &models.User{
Email: email,
UserName: username,
}
// Create user with empty password
if err := h.userService.CreateUser(user, ""); err != nil {
return nil, "", "", fmt.Errorf("failed to create user: %w", err)
}
// Assign default role
if err := h.userService.AssignDefaultRole(user.ID); err != nil {
// Log error but continue
// fmt.Printf("Failed to assign default role: %v\n", err)
}
}
// Check if social account exists
accounts, err := h.socialAccountService.GetSocialAccountsByUser(user.ID)
if err != nil {
return nil, "", "", err
}
// Create social account if it doesn't exist
found := false
for _, acc := range accounts {
if acc.Provider == provider && acc.ProviderID == providerUserID {
found = true
break
}
}
if !found {
socialAccount := &models.SocialAccount{
UserID: user.ID,
Provider: provider,
ProviderID: providerUserID,
}
if err := h.socialAccountService.CreateSocialAccount(socialAccount); err != nil {
return nil, "", "", fmt.Errorf("failed to create social account: %w", err)
}
}
// Generate JWT tokens
accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user)
if err != nil {
return nil, "", "", fmt.Errorf("failed to generate tokens: %w", err)
}
// Clear password before returning
user.Password = ""
return user, accessToken, refreshToken, nil
}

View File

@@ -0,0 +1,70 @@
package handlers
import (
"gobeyhan/app/account/services"
"gobeyhan/database/models"
"net/http"
"github.com/gin-gonic/gin"
)
type PermissionHandler struct {
service *services.PermissionService
}
func NewPermissionHandler(service *services.PermissionService) *PermissionHandler {
return &PermissionHandler{service: service}
}
// AdminGetAllPermissions godoc
// @Summary Get all permissions (Admin)
// @Description Get list of all permissions
// @Tags admin,permissions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.Permission
// @Router /api/v1/admin/permissions [get]
func (h *PermissionHandler) AdminGetAllPermissions(c *gin.Context) {
permissions, err := h.service.GetAllPermissions()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": permissions})
}
// AdminCreatePermission godoc
// @Summary Create a new permission (Admin)
// @Description Create a new permission
// @Tags admin,permissions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param permission body models.Permission true "Permission object"
// @Success 201 {object} models.Permission
// @Router /api/v1/admin/permissions [post]
func (h *PermissionHandler) AdminCreatePermission(c *gin.Context) {
var input struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
permission := &models.Permission{
Name: input.Name,
Description: input.Description,
}
if err := h.service.CreatePermission(permission); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": permission})
}

View File

@@ -0,0 +1,169 @@
package handlers
import (
"gobeyhan/app/account/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type RoleHandler struct {
service *services.RoleService
}
func NewRoleHandler(service *services.RoleService) *RoleHandler {
return &RoleHandler{service: service}
}
// AdminGetAllRoles godoc
// @Summary Get all roles (Admin)
// @Description Get list of all roles with permissions
// @Tags admin,roles
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.Role
// @Router /api/v1/admin/roles [get]
func (h *RoleHandler) AdminGetAllRoles(c *gin.Context) {
roles, err := h.service.GetAllRoles()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": roles})
}
// AdminGetRoleByID godoc
// @Summary Get role by ID (Admin)
// @Description Get a single role by ID
// @Tags admin,roles
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Role ID"
// @Success 200 {object} models.Role
// @Router /api/v1/admin/roles/{id} [get]
func (h *RoleHandler) AdminGetRoleByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
role, err := h.service.GetRoleByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if role == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Role not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": role})
}
// AdminCreateRole godoc
// @Summary Create a new role (Admin)
// @Description Create a new role
// @Tags admin,roles
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param role body models.Role true "Role object"
// @Success 201 {object} models.Role
// @Router /api/v1/admin/roles [post]
func (h *RoleHandler) AdminCreateRole(c *gin.Context) {
var input struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
role := &models.Role{
Name: input.Name,
Description: input.Description,
}
if err := h.service.CreateRole(role); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": role})
}
// AdminUpdateRole godoc
// @Summary Update a role (Admin)
// @Description Update an existing role
// @Tags admin,roles
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Role ID"
// @Param role body models.Role true "Role object"
// @Success 200 {object} models.Role
// @Router /api/v1/admin/roles/{id} [put]
func (h *RoleHandler) AdminUpdateRole(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateRole(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated role
role, err := h.service.GetRoleByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": role})
}
// AdminDeleteRole godoc
// @Summary Delete a role (Admin)
// @Description Delete a role by ID
// @Tags admin,roles
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Role ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/roles/{id} [delete]
func (h *RoleHandler) AdminDeleteRole(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
if err := h.service.DeleteRole(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role deleted successfully"})
}

View File

@@ -0,0 +1,111 @@
package handlers
import (
"gobeyhan/app/account/services"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type SocialAccountHandler struct {
service *services.SocialAccountService
}
func NewSocialAccountHandler(service *services.SocialAccountService) *SocialAccountHandler {
return &SocialAccountHandler{service: service}
}
// GetUserSocialAccounts godoc
// @Summary Get user's social accounts
// @Description Get all social accounts for the authenticated user
// @Tags social-accounts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.SocialAccount
// @Router /api/v1/user/social-accounts [get]
func (h *SocialAccountHandler) GetUserSocialAccounts(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
// Convert user_id to uint64
var uid uint64
switch v := userID.(type) {
case string:
uid, _ = strconv.ParseUint(v, 10, 64)
case uint64:
uid = v
case int:
uid = uint64(v)
case float64:
uid = uint64(v)
}
accounts, err := h.service.GetSocialAccountsByUser(uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": accounts})
}
// DeleteSocialAccount godoc
// @Summary Delete a social account
// @Description Delete a social account for the authenticated user
// @Tags social-accounts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Social Account ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/user/social-accounts/{id} [delete]
func (h *SocialAccountHandler) DeleteSocialAccount(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid social account ID"})
return
}
// Verify ownership
account, err := h.service.GetSocialAccountByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if account == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Social account not found"})
return
}
userID, _ := c.Get("user_id")
var uid uint64
switch v := userID.(type) {
case string:
uid, _ = strconv.ParseUint(v, 10, 64)
case uint64:
uid = v
case int:
uid = uint64(v)
case float64:
uid = uint64(v)
}
if account.UserID != uid {
c.JSON(http.StatusForbidden, gin.H{"error": "You can only delete your own social accounts"})
return
}
if err := h.service.DeleteSocialAccount(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Social account deleted successfully"})
}

View File

@@ -0,0 +1,287 @@
package handlers
import (
"gobeyhan/app/account/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
service *services.UserService
}
func NewUserHandler(service *services.UserService) *UserHandler {
return &UserHandler{service: service}
}
// AdminGetAllUsers godoc
// @Summary Get all users (Admin)
// @Description Get paginated list of all users
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Items per page" default(10)
// @Param include_deleted query bool false "Include soft-deleted users"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/admin/users [get]
func (h *UserHandler) AdminGetAllUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
includeDeleted := c.DefaultQuery("include_deleted", "false") == "true"
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 10
}
users, total, err := h.service.GetAllUsers(includeDeleted, page, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": users,
"total": total,
"page": page,
"limit": limit,
})
}
// AdminGetUserByID godoc
// @Summary Get user by ID (Admin)
// @Description Get a single user by ID
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 200 {object} models.User
// @Router /api/v1/admin/users/{id} [get]
func (h *UserHandler) AdminGetUserByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
user, err := h.service.GetUserByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if user == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
// AdminCreateUser godoc
// @Summary Create a new user (Admin)
// @Description Create a new user
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param user body models.User true "User object"
// @Success 201 {object} models.User
// @Router /api/v1/admin/users [post]
func (h *UserHandler) AdminCreateUser(c *gin.Context) {
var input struct {
UserName string `json:"username"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
Avatar string `json:"avatar"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user := &models.User{
UserName: input.UserName,
Email: input.Email,
Avatar: input.Avatar,
}
if err := h.service.CreateUser(user, input.Password); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": user})
}
// AdminUpdateUser godoc
// @Summary Update a user (Admin)
// @Description Update an existing user
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Param user body models.User true "User object"
// @Success 200 {object} models.User
// @Router /api/v1/admin/users/{id} [put]
func (h *UserHandler) AdminUpdateUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateUser(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated user
user, err := h.service.GetUserByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
// AdminDeleteUser godoc
// @Summary Delete a user (Admin)
// @Description Soft delete a user by ID
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/users/{id} [delete]
func (h *UserHandler) AdminDeleteUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
if err := h.service.DeleteUser(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
}
// AdminRestoreUser godoc
// @Summary Restore a deleted user (Admin)
// @Description Restore a soft-deleted user
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/users/{id}/restore [post]
func (h *UserHandler) AdminRestoreUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
if err := h.service.RestoreUser(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User restored successfully"})
}
// AdminAssignRole godoc
// @Summary Assign role to user (Admin)
// @Description Assign a role to a user
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Param role_id body int true "Role ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/users/{id}/roles [post]
func (h *UserHandler) AdminAssignRole(c *gin.Context) {
idStr := c.Param("id")
userID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
var input struct {
RoleID uint64 `json:"role_id" binding:"required"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.AssignRole(userID, input.RoleID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role assigned successfully"})
}
// AdminRemoveRole godoc
// @Summary Remove role from user (Admin)
// @Description Remove a role from a user
// @Tags admin,users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Param role_id path int true "Role ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/users/{id}/roles/{role_id} [delete]
func (h *UserHandler) AdminRemoveRole(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := strconv.ParseUint(userIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
roleIDStr := c.Param("role_id")
roleID, err := strconv.ParseUint(roleIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
if err := h.service.RemoveRole(userID, roleID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role removed successfully"})
}

View File

@@ -0,0 +1,42 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type PermissionService struct{}
func NewPermissionService() *PermissionService {
return &PermissionService{}
}
// GetAllPermissions retrieves all permissions
func (s *PermissionService) GetAllPermissions() ([]models.Permission, error) {
var permissions []models.Permission
err := database.DB.Find(&permissions).Error
return permissions, err
}
// GetPermissionByID retrieves a permission by ID
func (s *PermissionService) GetPermissionByID(id uint64) (*models.Permission, error) {
var permission models.Permission
err := database.DB.First(&permission, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &permission, nil
}
// CreatePermission creates a new permission
func (s *PermissionService) CreatePermission(permission *models.Permission) error {
return database.DB.Create(permission).Error
}

View File

@@ -0,0 +1,96 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type RoleService struct{}
func NewRoleService() *RoleService {
return &RoleService{}
}
// GetAllRoles retrieves all roles
func (s *RoleService) GetAllRoles() ([]models.Role, error) {
var roles []models.Role
err := database.DB.Preload("Permissions").Find(&roles).Error
return roles, err
}
// GetRoleByID retrieves a role by ID
func (s *RoleService) GetRoleByID(id uint64) (*models.Role, error) {
var role models.Role
err := database.DB.Preload("Permissions").First(&role, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &role, nil
}
// CreateRole creates a new role
func (s *RoleService) CreateRole(role *models.Role) error {
return database.DB.Create(role).Error
}
// UpdateRole updates an existing role
func (s *RoleService) UpdateRole(id uint64, updates map[string]interface{}) error {
result := database.DB.Model(&models.Role{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// DeleteRole deletes a role by ID
func (s *RoleService) DeleteRole(id uint64) error {
result := database.DB.Delete(&models.Role{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// AssignPermission assigns a permission to a role
func (s *RoleService) AssignPermission(roleID, permissionID uint64) error {
var role models.Role
if err := database.DB.First(&role, roleID).Error; err != nil {
return err
}
var permission models.Permission
if err := database.DB.First(&permission, permissionID).Error; err != nil {
return err
}
return database.DB.Model(&role).Association("Permissions").Append(&permission)
}
// RemovePermission removes a permission from a role
func (s *RoleService) RemovePermission(roleID, permissionID uint64) error {
var role models.Role
if err := database.DB.Preload("Permissions").First(&role, roleID).Error; err != nil {
return err
}
var permission models.Permission
if err := database.DB.First(&permission, permissionID).Error; err != nil {
return err
}
return database.DB.Model(&role).Association("Permissions").Delete(&permission)
}

View File

@@ -0,0 +1,54 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type SocialAccountService struct{}
func NewSocialAccountService() *SocialAccountService {
return &SocialAccountService{}
}
// GetSocialAccountsByUser retrieves all social accounts for a user
func (s *SocialAccountService) GetSocialAccountsByUser(userID uint64) ([]models.SocialAccount, error) {
var accounts []models.SocialAccount
err := database.DB.Where("user_id = ?", userID).Find(&accounts).Error
return accounts, err
}
// GetSocialAccountByID retrieves a social account by ID
func (s *SocialAccountService) GetSocialAccountByID(id uint64) (*models.SocialAccount, error) {
var account models.SocialAccount
err := database.DB.First(&account, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &account, nil
}
// CreateSocialAccount creates a new social account
func (s *SocialAccountService) CreateSocialAccount(account *models.SocialAccount) error {
return database.DB.Create(account).Error
}
// DeleteSocialAccount deletes a social account by ID
func (s *SocialAccountService) DeleteSocialAccount(id uint64) error {
result := database.DB.Delete(&models.SocialAccount{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}

View File

@@ -0,0 +1,184 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type UserService struct{}
func NewUserService() *UserService {
return &UserService{}
}
// GetAllUsers retrieves all users, optionally including soft-deleted ones
func (s *UserService) GetAllUsers(includeDeleted bool, page, limit int) ([]models.User, int64, error) {
var users []models.User
var total int64
query := database.DB.Preload("Roles").Preload("SocialAccounts")
if includeDeleted {
query = query.Unscoped()
}
query.Model(&models.User{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("created_at DESC").
Find(&users).Error
return users, total, err
}
// GetUserByID retrieves a user by ID
func (s *UserService) GetUserByID(id uint64) (*models.User, error) {
var user models.User
err := database.DB.
Preload("Roles").
Preload("SocialAccounts").
First(&user, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
// GetUserByEmail retrieves a user by email
func (s *UserService) GetUserByEmail(email string) (*models.User, error) {
var user models.User
err := database.DB.
Preload("Roles").
Preload("SocialAccounts").
Where("email = ?", email).
First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
// CreateUser creates a new user with hashed password
func (s *UserService) CreateUser(user *models.User, password string) error {
// Hash password if provided
if password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
user.Password = string(hashedPassword)
}
return database.DB.Create(user).Error
}
// VerifyPassword checks if the provided password matches the hashed password
func (s *UserService) VerifyPassword(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
// UpdateUser updates an existing user
func (s *UserService) UpdateUser(id uint64, updates map[string]interface{}) error {
// If password is being updated, hash it first
if password, ok := updates["password"].(string); ok && password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
updates["password"] = string(hashedPassword)
}
result := database.DB.Model(&models.User{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// DeleteUser soft deletes a user
func (s *UserService) DeleteUser(id uint64) error {
result := database.DB.Delete(&models.User{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// RestoreUser restores a soft-deleted user
func (s *UserService) RestoreUser(id uint64) error {
result := database.DB.Model(&models.User{}).Unscoped().
Where("id = ?", id).
Update("deleted_at", nil)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// AssignRole assigns a role to a user
func (s *UserService) AssignRole(userID, roleID uint64) error {
var user models.User
if err := database.DB.First(&user, userID).Error; err != nil {
return err
}
var role models.Role
if err := database.DB.First(&role, roleID).Error; err != nil {
return err
}
return database.DB.Model(&user).Association("Roles").Append(&role)
}
// RemoveRole removes a role from a user
func (s *UserService) RemoveRole(userID, roleID uint64) error {
var user models.User
if err := database.DB.Preload("Roles").First(&user, userID).Error; err != nil {
return err
}
var role models.Role
if err := database.DB.First(&role, roleID).Error; err != nil {
return err
}
return database.DB.Model(&user).Association("Roles").Delete(&role)
}
// AssignDefaultRole assigns the default 'user' role to a user
func (s *UserService) AssignDefaultRole(userID uint64) error {
var role models.Role
// Find role by name 'user'
if err := database.DB.Where("name = ?", "user").First(&role).Error; err != nil {
return err
}
return s.AssignRole(userID, role.ID)
}

View File

@@ -0,0 +1,235 @@
package handlers
import (
"gobeyhan/app/blog/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type CategoryHandler struct {
service *services.CategoryService
}
func NewCategoryHandler(service *services.CategoryService) *CategoryHandler {
return &CategoryHandler{service: service}
}
// GetAllCategories godoc
// @Summary Get all active categories
// @Description Get list of all active categories (public endpoint)
// @Tags categories
// @Accept json
// @Produce json
// @Success 200 {array} models.Category
// @Router /api/v1/categories [get]
func (h *CategoryHandler) GetAllCategories(c *gin.Context) {
categories, err := h.service.GetAllCategories(true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": categories})
}
// GetCategoryBySlug godoc
// @Summary Get category by slug
// @Description Get a single category by its slug (public endpoint)
// @Tags categories
// @Accept json
// @Produce json
// @Param slug path string true "Category Slug"
// @Success 200 {object} models.Category
// @Router /api/v1/categories/{slug} [get]
func (h *CategoryHandler) GetCategoryBySlug(c *gin.Context) {
slug := c.Param("slug")
category, err := h.service.GetCategoryBySlug(slug)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if category == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
// AdminGetAllCategories godoc
// @Summary Get all categories (Admin)
// @Description Get list of all categories including inactive ones
// @Tags admin,categories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.Category
// @Router /api/v1/admin/categories [get]
func (h *CategoryHandler) AdminGetAllCategories(c *gin.Context) {
categories, err := h.service.GetAllCategories(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": categories})
}
// GetCategoryByID godoc
// @Summary Get category by ID (Admin)
// @Description Get a single category by ID
// @Tags admin,categories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Category ID"
// @Success 200 {object} models.Category
// @Router /api/v1/admin/categories/{id} [get]
func (h *CategoryHandler) GetCategoryByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
category, err := h.service.GetCategoryByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if category == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
// CreateCategory godoc
// @Summary Create a new category (Admin)
// @Description Create a new category
// @Tags admin,categories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param category body models.Category true "Category object"
// @Success 201 {object} models.Category
// @Router /api/v1/admin/categories [post]
func (h *CategoryHandler) CreateCategory(c *gin.Context) {
var input struct {
Title string `json:"title" binding:"required"`
Keywords string `json:"keywords"`
Desc string `json:"description"`
IsActive *bool `json:"is_active"`
Order *int `json:"order"`
Slug string `json:"slug"`
ParentID *uint64 `json:"parent_id"`
Image string `json:"image"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
category := &models.Category{
Title: input.Title,
Keywords: input.Keywords,
Desc: input.Desc,
Slug: input.Slug,
ParentID: input.ParentID,
Image: input.Image,
}
if input.IsActive != nil {
category.IsActive = *input.IsActive
} else {
category.IsActive = true
}
if input.Order != nil {
category.Order = *input.Order
} else {
category.Order = 1
}
if err := h.service.CreateCategory(category); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": category})
}
// UpdateCategory godoc
// @Summary Update a category (Admin)
// @Description Update an existing category
// @Tags admin,categories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Category ID"
// @Param category body models.Category true "Category object"
// @Success 200 {object} models.Category
// @Router /api/v1/admin/categories/{id} [put]
func (h *CategoryHandler) UpdateCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateCategory(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated category
category, err := h.service.GetCategoryByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
// DeleteCategory godoc
// @Summary Delete a category (Admin)
// @Description Delete a category by ID
// @Tags admin,categories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Category ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/categories/{id} [delete]
func (h *CategoryHandler) DeleteCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
if err := h.service.DeleteCategory(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Category deleted successfully"})
}

View File

@@ -0,0 +1,111 @@
package handlers
import (
"gobeyhan/app/blog/services"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type CategoryViewHandler struct {
service *services.CategoryViewService
}
func NewCategoryViewHandler(service *services.CategoryViewService) *CategoryViewHandler {
return &CategoryViewHandler{service: service}
}
// TrackCategoryView godoc
// @Summary Track a category view
// @Description Record a view event for a category (public endpoint)
// @Tags category-views
// @Accept json
// @Produce json
// @Param id path int true "Category ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/categories/{id}/view [post]
func (h *CategoryViewHandler) TrackCategoryView(c *gin.Context) {
idStr := c.Param("id")
categoryID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
ipAddress := c.ClientIP()
userAgent := c.Request.UserAgent()
if err := h.service.TrackCategoryView(categoryID, ipAddress, userAgent); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "View tracked successfully"})
}
// AdminGetAllCategoryViews godoc
// @Summary Get all category views (Admin)
// @Description Get paginated list of all category views
// @Tags admin,category-views
// @Accept json
// @Produce json
// @Security BearerAuth
// @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 /api/v1/admin/category-views [get]
func (h *CategoryViewHandler) AdminGetAllCategoryViews(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
}
views, total, err := h.service.GetAllCategoryViews(page, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": views,
"total": total,
"page": page,
"limit": limit,
})
}
// GetCategoryViewStats godoc
// @Summary Get view stats for a category (Admin)
// @Description Get view count and details for a specific category
// @Tags admin,category-views
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Category ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/admin/categories/{id}/views [get]
func (h *CategoryViewHandler) GetCategoryViewStats(c *gin.Context) {
idStr := c.Param("id")
categoryID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
count, err := h.service.GetCategoryViewCount(categoryID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"category_id": categoryID,
"view_count": count,
})
}

View File

@@ -0,0 +1,245 @@
package handlers
import (
"gobeyhan/app/blog/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type CommentHandler struct {
service *services.CommentService
}
func NewCommentHandler(service *services.CommentService) *CommentHandler {
return &CommentHandler{service: service}
}
// GetPostComments godoc
// @Summary Get comments for a post
// @Description Get all active comments for a specific post (public endpoint)
// @Tags comments
// @Accept json
// @Produce json
// @Param id path int true "Post ID"
// @Success 200 {array} models.Comment
// @Router /api/v1/posts/{id}/comments [get]
func (h *CommentHandler) GetPostComments(c *gin.Context) {
idStr := c.Param("id")
postID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
return
}
comments, err := h.service.GetCommentsByPost(postID, true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": comments})
}
// CreatePostComment godoc
// @Summary Create a comment on a post
// @Description Create a new comment (requires authentication)
// @Tags comments
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Param comment body models.Comment true "Comment object"
// @Success 201 {object} models.Comment
// @Router /api/v1/posts/{id}/comments [post]
func (h *CommentHandler) CreatePostComment(c *gin.Context) {
idStr := c.Param("id")
postID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
return
}
// Get user ID from context (set by auth middleware)
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
var input struct {
Title string `json:"title"`
Body string `json:"body" binding:"required"`
ParentID *uint64 `json:"parent_id"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Convert user_id to uint64
var uid uint64
switch v := userID.(type) {
case string:
uid, _ = strconv.ParseUint(v, 10, 64)
case uint64:
uid = v
case int:
uid = uint64(v)
case float64:
uid = uint64(v)
}
comment := &models.Comment{
UserID: uid,
ProductID: postID,
Title: input.Title,
Body: input.Body,
ParentID: input.ParentID,
IsActive: true,
}
if err := h.service.CreateComment(comment); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": comment})
}
// AdminGetAllComments godoc
// @Summary Get all comments (Admin)
// @Description Get paginated list of all comments
// @Tags admin,comments
// @Accept json
// @Produce json
// @Security BearerAuth
// @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 /api/v1/admin/comments [get]
func (h *CommentHandler) AdminGetAllComments(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
}
comments, total, err := h.service.GetAllComments(page, limit, false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": comments,
"total": total,
"page": page,
"limit": limit,
})
}
// AdminGetCommentByID godoc
// @Summary Get comment by ID (Admin)
// @Description Get a single comment by ID
// @Tags admin,comments
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Comment ID"
// @Success 200 {object} models.Comment
// @Router /api/v1/admin/comments/{id} [get]
func (h *CommentHandler) AdminGetCommentByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid comment ID"})
return
}
comment, err := h.service.GetCommentByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if comment == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Comment not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": comment})
}
// AdminUpdateComment godoc
// @Summary Update a comment (Admin)
// @Description Update an existing comment
// @Tags admin,comments
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Comment ID"
// @Param comment body models.Comment true "Comment object"
// @Success 200 {object} models.Comment
// @Router /api/v1/admin/comments/{id} [put]
func (h *CommentHandler) AdminUpdateComment(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid comment ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateComment(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated comment
comment, err := h.service.GetCommentByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": comment})
}
// AdminDeleteComment godoc
// @Summary Delete a comment (Admin)
// @Description Delete a comment by ID
// @Tags admin,comments
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Comment ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/comments/{id} [delete]
func (h *CommentHandler) AdminDeleteComment(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid comment ID"})
return
}
if err := h.service.DeleteComment(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Comment deleted successfully"})
}

View File

@@ -0,0 +1,306 @@
package handlers
import (
"gobeyhan/app/blog/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type PostHandler struct {
service *services.PostService
}
func NewPostHandler(service *services.PostService) *PostHandler {
return &PostHandler{service: service}
}
// GetAllPosts godoc
// @Summary Get all active posts
// @Description Get paginated list of active posts (public endpoint)
// @Tags posts
// @Accept json
// @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 /api/v1/posts [get]
func (h *PostHandler) GetAllPosts(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
}
posts, total, err := h.service.GetAllPosts(page, limit, true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": posts,
"total": total,
"page": page,
"limit": limit,
})
}
// GetPostBySlug godoc
// @Summary Get post by slug
// @Description Get a single post by its slug (public endpoint)
// @Tags posts
// @Accept json
// @Produce json
// @Param slug path string true "Post Slug"
// @Success 200 {object} models.Post
// @Router /api/v1/posts/{slug} [get]
func (h *PostHandler) GetPostBySlug(c *gin.Context) {
slug := c.Param("slug")
post, err := h.service.GetPostBySlug(slug)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if post == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": post})
}
// AdminGetAllPosts godoc
// @Summary Get all posts (Admin)
// @Description Get paginated list of all posts including inactive
// @Tags admin,posts
// @Accept json
// @Produce json
// @Security BearerAuth
// @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 /api/v1/admin/posts [get]
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
}
posts, total, err := h.service.GetAllPosts(page, limit, false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": posts,
"total": total,
"page": page,
"limit": limit,
})
}
// GetPostByID godoc
// @Summary Get post by ID (Admin)
// @Description Get a single post by ID
// @Tags admin,posts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Success 200 {object} models.Post
// @Router /api/v1/admin/posts/{id} [get]
func (h *PostHandler) GetPostByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
return
}
post, err := h.service.GetPostByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if post == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": post})
}
// CreatePost godoc
// @Summary Create a new post (Admin)
// @Description Create a new post
// @Tags admin,posts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param post body models.Post true "Post object"
// @Success 201 {object} models.Post
// @Router /api/v1/admin/posts [post]
func (h *PostHandler) CreatePost(c *gin.Context) {
var input struct {
Title string `json:"title" binding:"required"`
UserID *uint64 `json:"user_id"`
Content string `json:"content"`
Keywords string `json:"keywords"`
Image string `json:"image"`
Thumb string `json:"thumb"`
Video string `json:"video"`
Slug string `json:"slug"`
IsActive *bool `json:"is_active"`
IsFront *bool `json:"is_front"`
ParentID *uint64 `json:"parent_id"`
Categories []uint64 `json:"categories"`
Tags []uint64 `json:"tags"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
post := &models.Post{
Title: input.Title,
UserID: input.UserID,
Content: input.Content,
Keywords: input.Keywords,
Image: input.Image,
Thumb: input.Thumb,
Video: input.Video,
Slug: input.Slug,
ParentID: input.ParentID,
}
if input.IsActive != nil {
post.IsActive = *input.IsActive
} else {
post.IsActive = true
}
if input.IsFront != nil {
post.IsFront = *input.IsFront
} else {
post.IsFront = true
}
// Create post first
if err := h.service.CreatePost(post); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Add categories and tags if provided
if len(input.Categories) > 0 || len(input.Tags) > 0 {
updates := make(map[string]interface{})
if len(input.Categories) > 0 {
var categories []*models.Category
for _, catID := range input.Categories {
categories = append(categories, &models.Category{ID: catID})
}
updates["categories"] = categories
}
if len(input.Tags) > 0 {
var tags []*models.Tag
for _, tagID := range input.Tags {
tags = append(tags, &models.Tag{ID: tagID})
}
updates["tags"] = tags
}
if err := h.service.UpdatePost(post.ID, updates); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
// Fetch created post with relationships
createdPost, _ := h.service.GetPostByID(post.ID)
c.JSON(http.StatusCreated, gin.H{"data": createdPost})
}
// UpdatePost godoc
// @Summary Update a post (Admin)
// @Description Update an existing post
// @Tags admin,posts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Param post body models.Post true "Post object"
// @Success 200 {object} models.Post
// @Router /api/v1/admin/posts/{id} [put]
func (h *PostHandler) UpdatePost(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdatePost(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated post
post, err := h.service.GetPostByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": post})
}
// DeletePost godoc
// @Summary Delete a post (Admin)
// @Description Delete a post by ID
// @Tags admin,posts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/posts/{id} [delete]
func (h *PostHandler) DeletePost(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
return
}
if err := h.service.DeletePost(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post deleted successfully"})
}

View File

@@ -0,0 +1,220 @@
package handlers
import (
"gobeyhan/app/blog/services"
"gobeyhan/database/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type TagHandler struct {
service *services.TagService
}
func NewTagHandler(service *services.TagService) *TagHandler {
return &TagHandler{service: service}
}
// GetAllTags godoc
// @Summary Get all active tags
// @Description Get list of all active tags (public endpoint)
// @Tags tags
// @Accept json
// @Produce json
// @Success 200 {array} models.Tag
// @Router /api/v1/tags [get]
func (h *TagHandler) GetAllTags(c *gin.Context) {
tags, err := h.service.GetAllTags(true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": tags})
}
// GetTagBySlug godoc
// @Summary Get tag by slug
// @Description Get a single tag by its slug (public endpoint)
// @Tags tags
// @Accept json
// @Produce json
// @Param slug path string true "Tag Slug"
// @Success 200 {object} models.Tag
// @Router /api/v1/tags/{slug} [get]
func (h *TagHandler) GetTagBySlug(c *gin.Context) {
slug := c.Param("slug")
tag, err := h.service.GetTagBySlug(slug)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if tag == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Tag not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
// AdminGetAllTags godoc
// @Summary Get all tags (Admin)
// @Description Get list of all tags including inactive ones
// @Tags admin,tags
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.Tag
// @Router /api/v1/admin/tags [get]
func (h *TagHandler) AdminGetAllTags(c *gin.Context) {
tags, err := h.service.GetAllTags(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": tags})
}
// GetTagByID godoc
// @Summary Get tag by ID (Admin)
// @Description Get a single tag by ID
// @Tags admin,tags
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Tag ID"
// @Success 200 {object} models.Tag
// @Router /api/v1/admin/tags/{id} [get]
func (h *TagHandler) GetTagByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
tag, err := h.service.GetTagByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if tag == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Tag not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
// CreateTag godoc
// @Summary Create a new tag (Admin)
// @Description Create a new tag
// @Tags admin,tags
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param tag body models.Tag true "Tag object"
// @Success 201 {object} models.Tag
// @Router /api/v1/admin/tags [post]
func (h *TagHandler) CreateTag(c *gin.Context) {
var input struct {
Tag string `json:"tag" binding:"required"`
Slug string `json:"slug"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tag := &models.Tag{
Tag: input.Tag,
Slug: input.Slug,
}
if input.IsActive != nil {
tag.IsActive = *input.IsActive
} else {
tag.IsActive = true
}
if err := h.service.CreateTag(tag); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": tag})
}
// UpdateTag godoc
// @Summary Update a tag (Admin)
// @Description Update an existing tag
// @Tags admin,tags
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Tag ID"
// @Param tag body models.Tag true "Tag object"
// @Success 200 {object} models.Tag
// @Router /api/v1/admin/tags/{id} [put]
func (h *TagHandler) UpdateTag(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateTag(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Fetch updated tag
tag, err := h.service.GetTagByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
// DeleteTag godoc
// @Summary Delete a tag (Admin)
// @Description Delete a tag by ID
// @Tags admin,tags
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Tag ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/tags/{id} [delete]
func (h *TagHandler) DeleteTag(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
if err := h.service.DeleteTag(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Tag deleted successfully"})
}

View File

@@ -0,0 +1,107 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type CategoryService struct{}
func NewCategoryService() *CategoryService {
return &CategoryService{}
}
// GetAllCategories retrieves all categories, optionally filtering by active status
func (s *CategoryService) GetAllCategories(activeOnly bool) ([]models.Category, error) {
var categories []models.Category
query := database.DB.Preload("Parent").Preload("Children")
if activeOnly {
query = query.Where("is_active = ?", true)
}
err := query.Order("`order` ASC, created_at DESC").Find(&categories).Error
return categories, err
}
// GetCategoryByID retrieves a category by ID with parent and children relationships
func (s *CategoryService) GetCategoryByID(id uint64) (*models.Category, error) {
var category models.Category
err := database.DB.
Preload("Parent").
Preload("Children").
First(&category, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &category, nil
}
// GetCategoryBySlug retrieves a category by slug
func (s *CategoryService) GetCategoryBySlug(slug string) (*models.Category, error) {
var category models.Category
err := database.DB.
Preload("Parent").
Preload("Children").
Where("slug = ?", slug).
First(&category).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &category, nil
}
// CreateCategory creates a new category
func (s *CategoryService) CreateCategory(category *models.Category) error {
return database.DB.Create(category).Error
}
// UpdateCategory updates an existing category
func (s *CategoryService) UpdateCategory(id uint64, updates map[string]interface{}) error {
result := database.DB.Model(&models.Category{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// DeleteCategory deletes a category by ID
func (s *CategoryService) DeleteCategory(id uint64) error {
result := database.DB.Delete(&models.Category{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// GetCategoriesByParent retrieves child categories of a parent
func (s *CategoryService) GetCategoriesByParent(parentID uint64, activeOnly bool) ([]models.Category, error) {
var categories []models.Category
query := database.DB.Where("parent_id = ?", parentID)
if activeOnly {
query = query.Where("is_active = ?", true)
}
err := query.Order("`order` ASC, created_at DESC").Find(&categories).Error
return categories, err
}

View File

@@ -0,0 +1,67 @@
package services
import (
"gobeyhan/database"
"gobeyhan/database/models"
)
type CategoryViewService struct{}
func NewCategoryViewService() *CategoryViewService {
return &CategoryViewService{}
}
// TrackCategoryView records a category view
func (s *CategoryViewService) TrackCategoryView(categoryID uint64, ipAddress, userAgent string) error {
view := &models.CategoryView{
CategoryID: categoryID,
IPAddress: ipAddress,
UserAgent: userAgent,
}
return database.DB.Create(view).Error
}
// GetCategoryViewCount gets total view count for a category
func (s *CategoryViewService) GetCategoryViewCount(categoryID uint64) (int64, error) {
var count int64
err := database.DB.Model(&models.CategoryView{}).
Where("category_id = ?", categoryID).
Count(&count).Error
return count, err
}
// GetAllCategoryViews gets all views for a category with pagination
func (s *CategoryViewService) GetAllCategoryViews(page, limit int) ([]models.CategoryView, int64, error) {
var views []models.CategoryView
var total int64
query := database.DB.Preload("Category")
query.Model(&models.CategoryView{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("created_at DESC").
Find(&views).Error
return views, total, err
}
// GetViewsByCategory gets views for a specific category
func (s *CategoryViewService) GetViewsByCategory(categoryID uint64, page, limit int) ([]models.CategoryView, int64, error) {
var views []models.CategoryView
var total int64
query := database.DB.Where("category_id = ?", categoryID)
query.Model(&models.CategoryView{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("created_at DESC").
Find(&views).Error
return views, total, err
}

View File

@@ -0,0 +1,115 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type CommentService struct{}
func NewCommentService() *CommentService {
return &CommentService{}
}
// GetCommentsByPost retrieves comments for a specific post
func (s *CommentService) GetCommentsByPost(postID uint64, activeOnly bool) ([]models.Comment, error) {
var comments []models.Comment
query := database.DB.
Where("product_id = ?", postID).
Preload("Parent").
Preload("Children")
if activeOnly {
query = query.Where("is_active = ?", true)
}
err := query.Order("created_at DESC").Find(&comments).Error
return comments, err
}
// GetAllComments retrieves all comments with pagination
func (s *CommentService) GetAllComments(page, limit int, activeOnly bool) ([]models.Comment, int64, error) {
var comments []models.Comment
var total int64
query := database.DB.
Preload("Product").
Preload("Parent").
Preload("Children")
if activeOnly {
query = query.Where("is_active = ?", true)
}
query.Model(&models.Comment{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("created_at DESC").
Find(&comments).Error
return comments, total, err
}
// GetCommentByID retrieves a comment by ID
func (s *CommentService) GetCommentByID(id uint64) (*models.Comment, error) {
var comment models.Comment
err := database.DB.
Preload("Product").
Preload("Parent").
Preload("Children").
First(&comment, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &comment, nil
}
// CreateComment creates a new comment
func (s *CommentService) CreateComment(comment *models.Comment) error {
return database.DB.Create(comment).Error
}
// UpdateComment updates an existing comment
func (s *CommentService) UpdateComment(id uint64, updates map[string]interface{}) error {
result := database.DB.Model(&models.Comment{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// DeleteComment deletes a comment by ID
func (s *CommentService) DeleteComment(id uint64) error {
result := database.DB.Delete(&models.Comment{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// GetCommentReplies retrieves replies to a specific comment
func (s *CommentService) GetCommentReplies(commentID uint64) ([]models.Comment, error) {
var replies []models.Comment
err := database.DB.
Where("parent_id = ? AND is_active = ?", commentID, true).
Order("created_at ASC").
Find(&replies).Error
return replies, err
}

View File

@@ -0,0 +1,202 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type PostService struct{}
func NewPostService() *PostService {
return &PostService{}
}
// GetAllPosts retrieves all posts with pagination, optionally filtering by active status
func (s *PostService) GetAllPosts(page, limit int, activeOnly bool) ([]models.Post, int64, error) {
var posts []models.Post
var total int64
query := database.DB.
Preload("User").
Preload("Categories").
Preload("Tags").
Preload("Parent").
Preload("Children")
if activeOnly {
query = query.Where("is_active = ?", true)
}
// Count total
query.Model(&models.Post{}).Count(&total)
// Get paginated results
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("created_at DESC").
Find(&posts).Error
return posts, total, err
}
// GetPostByID retrieves a post by ID with all relationships
func (s *PostService) GetPostByID(id uint64) (*models.Post, error) {
var post models.Post
err := database.DB.
Preload("User").
Preload("Categories").
Preload("Tags").
Preload("Parent").
Preload("Children").
First(&post, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &post, nil
}
// GetPostBySlug retrieves a post by slug
func (s *PostService) GetPostBySlug(slug string) (*models.Post, error) {
var post models.Post
err := database.DB.
Preload("User").
Preload("Categories").
Preload("Tags").
Preload("Parent").
Preload("Children").
Where("slug = ? AND is_active = ?", slug, true).
First(&post).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &post, nil
}
// CreatePost creates a new post
func (s *PostService) CreatePost(post *models.Post) error {
return database.DB.Create(post).Error
}
// UpdatePost updates an existing post
func (s *PostService) UpdatePost(id uint64, updates map[string]interface{}) error {
// Handle many-to-many relationships separately if they're in updates
var categoryIDs []*models.Category
var tagIDs []*models.Tag
if categories, ok := updates["categories"]; ok {
if catSlice, ok := categories.([]*models.Category); ok {
categoryIDs = catSlice
delete(updates, "categories")
}
}
if tags, ok := updates["tags"]; ok {
if tagSlice, ok := tags.([]*models.Tag); ok {
tagIDs = tagSlice
delete(updates, "tags")
}
}
// Update basic fields
result := database.DB.Model(&models.Post{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
// Update relationships if provided
if len(categoryIDs) > 0 || len(tagIDs) > 0 {
var post models.Post
if err := database.DB.First(&post, id).Error; err != nil {
return err
}
if len(categoryIDs) > 0 {
if err := database.DB.Model(&post).Association("Categories").Replace(categoryIDs); err != nil {
return err
}
}
if len(tagIDs) > 0 {
if err := database.DB.Model(&post).Association("Tags").Replace(tagIDs); err != nil {
return err
}
}
}
return nil
}
// DeletePost deletes a post by ID
func (s *PostService) DeletePost(id uint64) error {
result := database.DB.Delete(&models.Post{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// GetPostsByCategory retrieves posts by category ID
func (s *PostService) GetPostsByCategory(categoryID uint64, page, limit int) ([]models.Post, int64, error) {
var posts []models.Post
var total int64
query := database.DB.
Joins("JOIN post_categories ON post_categories.post_id = posts.id").
Where("post_categories.category_id = ? AND posts.is_active = ?", categoryID, true).
Preload("User").
Preload("Categories").
Preload("Tags")
query.Model(&models.Post{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("posts.created_at DESC").
Find(&posts).Error
return posts, total, err
}
// GetPostsByTag retrieves posts by tag ID
func (s *PostService) GetPostsByTag(tagID uint64, page, limit int) ([]models.Post, int64, error) {
var posts []models.Post
var total int64
query := database.DB.
Joins("JOIN post_tags ON post_tags.post_id = posts.id").
Where("post_tags.tag_id = ? AND posts.is_active = ?", tagID, true).
Preload("User").
Preload("Categories").
Preload("Tags")
query.Model(&models.Post{}).Count(&total)
err := query.
Offset((page - 1) * limit).
Limit(limit).
Order("posts.created_at DESC").
Find(&posts).Error
return posts, total, err
}

View File

@@ -0,0 +1,87 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"gorm.io/gorm"
)
type TagService struct{}
func NewTagService() *TagService {
return &TagService{}
}
// GetAllTags retrieves all tags, optionally filtering by active status
func (s *TagService) GetAllTags(activeOnly bool) ([]models.Tag, error) {
var tags []models.Tag
query := database.DB
if activeOnly {
query = query.Where("is_active = ?", true)
}
err := query.Order("tag ASC").Find(&tags).Error
return tags, err
}
// GetTagByID retrieves a tag by ID
func (s *TagService) GetTagByID(id uint64) (*models.Tag, error) {
var tag models.Tag
err := database.DB.First(&tag, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &tag, nil
}
// GetTagBySlug retrieves a tag by slug
func (s *TagService) GetTagBySlug(slug string) (*models.Tag, error) {
var tag models.Tag
err := database.DB.Where("slug = ?", slug).First(&tag).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &tag, nil
}
// CreateTag creates a new tag
func (s *TagService) CreateTag(tag *models.Tag) error {
return database.DB.Create(tag).Error
}
// UpdateTag updates an existing tag
func (s *TagService) UpdateTag(id uint64, updates map[string]interface{}) error {
result := database.DB.Model(&models.Tag{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// DeleteTag deletes a tag by ID
func (s *TagService) DeleteTag(id uint64) error {
result := database.DB.Delete(&models.Tag{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}

View File

@@ -0,0 +1,49 @@
package middlewares
import (
"net/http"
"gobeyhan/database"
"gobeyhan/database/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 (
"gobeyhan/app/settings/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 (
"gobeyhan/app/settings/services"
"gobeyhan/config"
"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"
"gobeyhan/app/settings/services"
"gobeyhan/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
}

302
app/routes/routes.go Normal file
View File

@@ -0,0 +1,302 @@
package routes
import (
accountHandlers "gobeyhan/app/account/handlers"
accountServices "gobeyhan/app/account/services"
blogHandlers "gobeyhan/app/blog/handlers"
blogServices "gobeyhan/app/blog/services"
"gobeyhan/app/middlewares"
settingsHandlers "gobeyhan/app/settings/handlers"
settingsServices "gobeyhan/app/settings/services"
adminPkg "gobeyhan/internal/handler/admin"
"github.com/gin-gonic/gin"
)
// SetupRoutes initializes all application routes
func SetupRoutes(r *gin.Engine) {
// ============================================
// BLOG APP - Services & Handlers
// ============================================
categoryService := blogServices.NewCategoryService()
tagService := blogServices.NewTagService()
postService := blogServices.NewPostService()
commentService := blogServices.NewCommentService()
categoryViewService := blogServices.NewCategoryViewService()
categoryHandler := blogHandlers.NewCategoryHandler(categoryService)
tagHandler := blogHandlers.NewTagHandler(tagService)
postHandler := blogHandlers.NewPostHandler(postService)
commentHandler := blogHandlers.NewCommentHandler(commentService)
categoryViewHandler := blogHandlers.NewCategoryViewHandler(categoryViewService)
// ============================================
// ACCOUNT APP - Services & Handlers
// ============================================
// ============================================
// ACCOUNT APP - Services & Handlers
// ============================================
userService := accountServices.NewUserService()
socialAccountService := accountServices.NewSocialAccountService()
roleService := accountServices.NewRoleService()
permissionService := accountServices.NewPermissionService()
// Settings & Utils
settingsService := settingsServices.NewSettingsService()
jwtService := settingsServices.NewJWTService()
// Handlers
userHandler := accountHandlers.NewUserHandler(userService)
authHandler := accountHandlers.NewAuthHandler(userService, jwtService)
oauthHandler := accountHandlers.NewOAuthHandler(userService, socialAccountService, jwtService)
// socialAccountHandler := accountHandlers.NewSocialAccountHandler(socialAccountService)
roleHandler := accountHandlers.NewRoleHandler(roleService)
permissionHandler := accountHandlers.NewPermissionHandler(permissionService)
// ============================================
// SETTINGS APP - Services & Handlers
// ============================================
settingsHandler := settingsHandlers.NewSettingsHandler(settingsService)
// ============================================
// ADMIN UI ROUTES
// ============================================
adminHandler := adminPkg.NewHandler()
r.GET("/admin/login", adminHandler.LoginPage)
r.POST("/admin/login", adminHandler.LoginPost)
r.GET("/admin", func(c *gin.Context) {
c.Redirect(301, "/admin/dashboard")
})
r.GET("/admin/dashboard", adminHandler.Dashboard)
// User CRUD
adminUserHandler := adminPkg.NewUserHandler()
r.GET("/admin/users", adminUserHandler.List)
r.GET("/admin/users/new", adminUserHandler.New)
r.POST("/admin/users", adminUserHandler.Create)
r.GET("/admin/users/:id/edit", adminUserHandler.Edit)
r.POST("/admin/users/:id", adminUserHandler.Update)
r.POST("/admin/users/:id/delete", adminUserHandler.Delete)
// ========================================
// SETTINGS UI ROUTES
// ========================================
adminSettingsHandler := adminPkg.NewSettingsHandler()
// Whitelist
r.GET("/admin/settings/whitelist", adminSettingsHandler.ListWhitelist)
r.GET("/admin/settings/whitelist/new", adminSettingsHandler.NewWhitelist)
r.POST("/admin/settings/whitelist", adminSettingsHandler.CreateWhitelist)
r.GET("/admin/settings/whitelist/:id/edit", adminSettingsHandler.EditWhitelist)
r.POST("/admin/settings/whitelist/:id", adminSettingsHandler.UpdateWhitelist)
r.POST("/admin/settings/whitelist/:id/delete", adminSettingsHandler.DeleteWhitelist)
// Blacklist
r.GET("/admin/settings/blacklist", adminSettingsHandler.ListBlacklist)
r.GET("/admin/settings/blacklist/new", adminSettingsHandler.NewBlacklist)
r.POST("/admin/settings/blacklist", adminSettingsHandler.CreateBlacklist)
r.GET("/admin/settings/blacklist/:id/edit", adminSettingsHandler.EditBlacklist)
r.POST("/admin/settings/blacklist/:id", adminSettingsHandler.UpdateBlacklist)
r.POST("/admin/settings/blacklist/:id/delete", adminSettingsHandler.DeleteBlacklist)
// Rate Limits
r.GET("/admin/settings/rate-limits", adminSettingsHandler.ListRateLimits)
r.GET("/admin/settings/rate-limits/:id/edit", adminSettingsHandler.EditRateLimit)
r.POST("/admin/settings/rate-limits/:id", adminSettingsHandler.UpdateRateLimit)
r.POST("/admin/settings/rate-limits/:id/delete", adminSettingsHandler.DeleteRateLimit)
// ========================================
// BLOG UI ROUTES
// ========================================
adminBlogHandler := adminPkg.NewBlogHandler()
r.GET("/admin/blog", adminBlogHandler.List)
r.GET("/admin/blog/new", adminBlogHandler.New)
r.POST("/admin/blog", adminBlogHandler.Create)
r.GET("/admin/blog/:id/edit", adminBlogHandler.Edit)
r.POST("/admin/blog/:id", adminBlogHandler.Update)
r.POST("/admin/blog/:id/delete", adminBlogHandler.Delete)
// Categories
r.GET("/admin/blog/categories", adminBlogHandler.ListCategories)
r.GET("/admin/blog/categories/new", adminBlogHandler.NewCategory)
r.POST("/admin/blog/categories", adminBlogHandler.CreateCategory)
r.GET("/admin/blog/categories/:id/edit", adminBlogHandler.EditCategory)
r.POST("/admin/blog/categories/:id", adminBlogHandler.UpdateCategory)
r.POST("/admin/blog/categories/:id/delete", adminBlogHandler.DeleteCategory)
// Tags
r.GET("/admin/blog/tags", adminBlogHandler.ListTags)
r.GET("/admin/blog/tags/new", adminBlogHandler.NewTag)
r.POST("/admin/blog/tags", adminBlogHandler.CreateTag)
r.GET("/admin/blog/tags/:id/edit", adminBlogHandler.EditTag)
r.POST("/admin/blog/tags/:id", adminBlogHandler.UpdateTag)
r.POST("/admin/blog/tags/:id/delete", adminBlogHandler.DeleteTag)
// Comments
r.GET("/admin/blog/comments", adminBlogHandler.ListComments)
r.GET("/admin/blog/comments/:id/edit", adminBlogHandler.EditComment)
r.POST("/admin/blog/comments/:id", adminBlogHandler.UpdateComment)
r.POST("/admin/blog/comments/:id/delete", adminBlogHandler.DeleteComment)
// Static files sharing
r.Static("/uploads", "./uploads")
// ============================================
// API v1 Group
// ============================================
api := r.Group("/api/v1")
api.Use(middlewares.DynamicCorsMiddleware(settingsService))
{
// ========================================
// AUTH ENDPOINTS
// ========================================
auth := api.Group("/auth")
{
// Basic Auth
auth.POST("/register", authHandler.Register)
auth.POST("/login", authHandler.Login)
auth.POST("/refresh", authHandler.RefreshToken)
auth.POST("/logout", authHandler.Logout)
// OAuth
auth.GET("/google", oauthHandler.GoogleLogin)
auth.GET("/google/callback", oauthHandler.GoogleCallback)
auth.GET("/github", oauthHandler.GithubLogin)
auth.GET("/github/callback", oauthHandler.GithubCallback)
// Protected
auth.GET("/me", middlewares.AuthMiddleware(jwtService), authHandler.GetCurrentUser)
}
// ========================================
// PUBLIC ENDPOINTS (Read-only)
// ========================================
// Blog - Categories
api.GET("/categories", categoryHandler.GetAllCategories)
api.GET("/categories/:slug", categoryHandler.GetCategoryBySlug)
api.POST("/categories/:id/view", categoryViewHandler.TrackCategoryView)
// Blog - Tags
api.GET("/tags", tagHandler.GetAllTags)
api.GET("/tags/:slug", tagHandler.GetTagBySlug)
// Blog - Posts
api.GET("/posts", postHandler.GetAllPosts)
api.GET("/posts/:slug", postHandler.GetPostBySlug)
// Blog - Comments (separate route to avoid wildcard conflict)
api.GET("/comments/post/:postId", commentHandler.GetPostComments)
// ========================================
// AUTHENTICATED USER ENDPOINTS
// ========================================
// NOTE: These routes require AuthMiddleware()
// Uncomment when authentication middleware is ready
// user := api.Group("/user")
// user.Use(AuthMiddleware())
// {
// // Blog - Comments (authenticated users can comment)
// user.POST("/comments/post/:postId", commentHandler.CreatePostComment)
//
// // Account - Social Accounts
// user.GET("/social-accounts", socialAccountHandler.GetUserSocialAccounts)
// user.DELETE("/social-accounts/:id", socialAccountHandler.DeleteSocialAccount)
// }
// ========================================
// ADMIN ENDPOINTS (Protected)
// ========================================
// NOTE: These routes require AuthMiddleware() + AdminMiddleware()
admin := api.Group("/admin")
admin.Use(middlewares.AuthMiddleware(jwtService), middlewares.AdminMiddleware())
{
// ========================================
// BLOG APP - Admin Routes
// ========================================
// Categories
admin.GET("/categories", categoryHandler.AdminGetAllCategories)
admin.GET("/categories/:id", categoryHandler.GetCategoryByID)
admin.POST("/categories", categoryHandler.CreateCategory)
admin.PUT("/categories/:id", categoryHandler.UpdateCategory)
admin.DELETE("/categories/:id", categoryHandler.DeleteCategory)
admin.GET("/categories/:id/views", categoryViewHandler.GetCategoryViewStats)
// Tags
admin.GET("/tags", tagHandler.AdminGetAllTags)
admin.GET("/tags/:id", tagHandler.GetTagByID)
admin.POST("/tags", tagHandler.CreateTag)
admin.PUT("/tags/:id", tagHandler.UpdateTag)
admin.DELETE("/tags/:id", tagHandler.DeleteTag)
// Posts
admin.GET("/posts", postHandler.AdminGetAllPosts)
admin.GET("/posts/:id", postHandler.GetPostByID)
admin.POST("/posts", postHandler.CreatePost)
admin.PUT("/posts/:id", postHandler.UpdatePost)
admin.DELETE("/posts/:id", postHandler.DeletePost)
// Comments
admin.GET("/comments", commentHandler.AdminGetAllComments)
admin.GET("/comments/:id", commentHandler.AdminGetCommentByID)
admin.PUT("/comments/:id", commentHandler.AdminUpdateComment)
admin.DELETE("/comments/:id", commentHandler.AdminDeleteComment)
// Category Views
admin.GET("/category-views", categoryViewHandler.AdminGetAllCategoryViews)
// ========================================
// ACCOUNT APP - Admin Routes
// ========================================
// Users
admin.GET("/users", userHandler.AdminGetAllUsers)
admin.GET("/users/:id", userHandler.AdminGetUserByID)
admin.POST("/users", userHandler.AdminCreateUser)
admin.PUT("/users/:id", userHandler.AdminUpdateUser)
admin.DELETE("/users/:id", userHandler.AdminDeleteUser)
admin.POST("/users/:id/restore", userHandler.AdminRestoreUser)
admin.POST("/users/:id/roles", userHandler.AdminAssignRole)
admin.DELETE("/users/:id/roles/:role_id", userHandler.AdminRemoveRole)
// Roles
admin.GET("/roles", roleHandler.AdminGetAllRoles)
admin.GET("/roles/:id", roleHandler.AdminGetRoleByID)
admin.POST("/roles", roleHandler.AdminCreateRole)
admin.PUT("/roles/:id", roleHandler.AdminUpdateRole)
admin.DELETE("/roles/:id", roleHandler.AdminDeleteRole)
// Permissions
admin.GET("/permissions", permissionHandler.AdminGetAllPermissions)
admin.POST("/permissions", permissionHandler.AdminCreatePermission)
// ========================================
// SETTINGS APP - Admin Routes
// ========================================
// CORS Whitelist
admin.GET("/cors/whitelist", settingsHandler.GetAllWhitelist)
admin.POST("/cors/whitelist", settingsHandler.CreateWhitelist)
admin.PUT("/cors/whitelist/:id", settingsHandler.UpdateWhitelist)
admin.DELETE("/cors/whitelist/:id", settingsHandler.DeleteWhitelist)
// CORS Blacklist
admin.GET("/cors/blacklist", settingsHandler.GetAllBlacklist)
admin.POST("/cors/blacklist", settingsHandler.CreateBlacklist)
admin.PUT("/cors/blacklist/:id", settingsHandler.UpdateBlacklist)
admin.DELETE("/cors/blacklist/:id", settingsHandler.DeleteBlacklist)
// CORS Cache
admin.POST("/cors/cache/invalidate", settingsHandler.InvalidateCorsCache)
// Rate Limits
admin.GET("/rate-limits", settingsHandler.GetAllRateLimits)
admin.PUT("/rate-limits/:id", settingsHandler.UpdateRateLimit)
}
}
}

462
app/routes/routes.go.backup Normal file
View File

@@ -0,0 +1,462 @@
package routes
import (
"gobeyhan/app/middlewares"
"gobeyhan/app/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("/api/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)
}
}
}
}

View File

@@ -0,0 +1,264 @@
package handlers
import (
"gobeyhan/app/settings/services"
"gobeyhan/database/models"
"net/http"
"github.com/gin-gonic/gin"
)
type SettingsHandler struct {
service *services.SettingsService
}
func NewSettingsHandler(service *services.SettingsService) *SettingsHandler {
return &SettingsHandler{service: service}
}
// GetAllWhitelist godoc
// @Summary Get all CORS whitelist entries (Admin)
// @Description Get all CORS whitelist origins
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.CorsWhitelist
// @Router /api/v1/admin/cors/whitelist [get]
func (h *SettingsHandler) GetAllWhitelist(c *gin.Context) {
whitelist, err := h.service.GetAllCorsWhitelist()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": whitelist})
}
// CreateWhitelist godoc
// @Summary Create CORS whitelist entry (Admin)
// @Description Add a new origin to CORS whitelist
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param whitelist body models.CorsWhitelist true "Whitelist object"
// @Success 201 {object} models.CorsWhitelist
// @Router /api/v1/admin/cors/whitelist [post]
func (h *SettingsHandler) CreateWhitelist(c *gin.Context) {
var input models.CorsWhitelist
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.CreateCorsWhitelist(&input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": input})
}
// UpdateWhitelist godoc
// @Summary Update CORS whitelist entry (Admin)
// @Description Update an existing CORS whitelist entry
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Whitelist ID"
// @Param whitelist body models.CorsWhitelist true "Whitelist object"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/cors/whitelist/{id} [put]
func (h *SettingsHandler) UpdateWhitelist(c *gin.Context) {
id := c.Param("id")
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateCorsWhitelist(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Whitelist updated successfully"})
}
// DeleteWhitelist godoc
// @Summary Delete CORS whitelist entry (Admin)
// @Description Delete a CORS whitelist entry
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Whitelist ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/cors/whitelist/{id} [delete]
func (h *SettingsHandler) DeleteWhitelist(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteCorsWhitelist(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Whitelist deleted successfully"})
}
// GetAllBlacklist godoc
// @Summary Get all CORS blacklist entries (Admin)
// @Description Get all CORS blacklist origins
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.CorsBlacklist
// @Router /api/v1/admin/cors/blacklist [get]
func (h *SettingsHandler) GetAllBlacklist(c *gin.Context) {
blacklist, err := h.service.GetAllCorsBlacklist()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": blacklist})
}
// CreateBlacklist godoc
// @Summary Create CORS blacklist entry (Admin)
// @Description Add a new origin to CORS blacklist
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param blacklist body models.CorsBlacklist true "Blacklist object"
// @Success 201 {object} models.CorsBlacklist
// @Router /api/v1/admin/cors/blacklist [post]
func (h *SettingsHandler) CreateBlacklist(c *gin.Context) {
var input models.CorsBlacklist
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.CreateCorsBlacklist(&input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": input})
}
// UpdateBlacklist godoc
// @Summary Update CORS blacklist entry (Admin)
// @Description Update an existing CORS blacklist entry
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Blacklist ID"
// @Param blacklist body models.CorsBlacklist true "Blacklist object"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/cors/blacklist/{id} [put]
func (h *SettingsHandler) UpdateBlacklist(c *gin.Context) {
id := c.Param("id")
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateCorsBlacklist(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Blacklist updated successfully"})
}
// DeleteBlacklist godoc
// @Summary Delete CORS blacklist entry (Admin)
// @Description Delete a CORS blacklist entry
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Blacklist ID"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/cors/blacklist/{id} [delete]
func (h *SettingsHandler) DeleteBlacklist(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteCorsBlacklist(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Blacklist deleted successfully"})
}
// GetAllRateLimits godoc
// @Summary Get all rate limit settings (Admin)
// @Description Get all rate limit configurations
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} models.RateLimitSetting
// @Router /api/v1/admin/rate-limits [get]
func (h *SettingsHandler) GetAllRateLimits(c *gin.Context) {
settings, err := h.service.GetAllRateLimitSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": settings})
}
// UpdateRateLimit godoc
// @Summary Update rate limit setting (Admin)
// @Description Update an existing rate limit configuration
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Rate Limit ID"
// @Param setting body models.RateLimitSetting true "Rate limit object"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/rate-limits/{id} [put]
func (h *SettingsHandler) UpdateRateLimit(c *gin.Context) {
id := c.Param("id")
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.UpdateRateLimitSetting(id, input); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Rate limit updated successfully"})
}
// InvalidateCorsCache godoc
// @Summary Invalidate CORS cache (Admin)
// @Description Clear the CORS cache to force reload from database
// @Tags admin,settings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/cors/cache/invalidate [post]
func (h *SettingsHandler) InvalidateCorsCache(c *gin.Context) {
h.service.InvalidateCorsCache()
c.JSON(http.StatusOK, gin.H{"message": "CORS cache invalidated successfully"})
}

View File

@@ -0,0 +1,204 @@
package services
import (
"encoding/json"
"gobeyhan/database"
"gobeyhan/database/models"
"time"
"github.com/redis/go-redis/v9"
)
type CacheService struct{}
func NewCacheService() *CacheService {
return &CacheService{}
}
// User Cache
func (s *CacheService) SetUser(userID string, user *models.User, expiration time.Duration) error {
userData, err := json.Marshal(user)
if err != nil {
return err
}
return database.Set("user:"+userID, userData, expiration)
}
func (s *CacheService) GetUser(userID string) (*models.User, error) {
data, err := database.Get("user:" + userID)
if err != nil {
return nil, err
}
var user models.User
err = json.Unmarshal([]byte(data), &user)
if err != nil {
return nil, err
}
return &user, nil
}
func (s *CacheService) DeleteUser(userID string) error {
return database.Delete("user:" + userID)
}
// Session Management
func (s *CacheService) SetSession(token string, userID string, expiration time.Duration) error {
return database.Set("session:"+token, userID, expiration)
}
func (s *CacheService) GetSession(token string) (string, error) {
return database.Get("session:" + token)
}
func (s *CacheService) DeleteSession(token string) error {
return database.Delete("session:" + token)
}
// Rate Limiting
func (s *CacheService) IncrementRateLimit(key string, expiration time.Duration) (int64, error) {
count, err := database.Increment("ratelimit:" + key)
if err != nil {
return 0, err
}
// Set expiration only for first increment
if count == 1 {
database.Expire("ratelimit:"+key, expiration)
}
return count, nil
}
func (s *CacheService) GetRateLimit(key string) (string, error) {
return database.Get("ratelimit:" + key)
}
// Token Blacklist (for logout)
func (s *CacheService) BlacklistToken(token string, expiration time.Duration) error {
return database.Set("blacklist:"+token, "1", expiration)
}
func (s *CacheService) IsTokenBlacklisted(token string) (bool, error) {
return database.Exists("blacklist:" + token)
}
// Email Verification Token Cache
func (s *CacheService) SetEmailVerification(email string, token string, expiration time.Duration) error {
return database.Set("email_verify:"+email, token, expiration)
}
func (s *CacheService) GetEmailVerification(email string) (string, error) {
return database.Get("email_verify:" + email)
}
func (s *CacheService) DeleteEmailVerification(email string) error {
return database.Delete("email_verify:" + email)
}
// Password Reset Token Cache
func (s *CacheService) SetPasswordReset(email string, token string, expiration time.Duration) error {
return database.Set("password_reset:"+email, token, expiration)
}
func (s *CacheService) GetPasswordReset(email string) (string, error) {
return database.Get("password_reset:" + email)
}
func (s *CacheService) DeletePasswordReset(email string) error {
return database.Delete("password_reset:" + email)
}
// CORS Whitelist Cache
func (s *CacheService) SetCorsWhitelist(origins []string, expiration time.Duration) error {
data, err := json.Marshal(origins)
if err != nil {
return err
}
return database.Set("cors:whitelist", data, expiration)
}
func (s *CacheService) GetCorsWhitelist() ([]string, error) {
data, err := database.Get("cors:whitelist")
if err != nil {
if err == redis.Nil {
return nil, nil
}
return nil, err
}
var origins []string
err = json.Unmarshal([]byte(data), &origins)
if err != nil {
return nil, err
}
return origins, nil
}
func (s *CacheService) InvalidateCorsWhitelist() error {
return database.Delete("cors:whitelist")
}
// CORS Blacklist Cache
func (s *CacheService) SetCorsBlacklist(origins []string, expiration time.Duration) error {
data, err := json.Marshal(origins)
if err != nil {
return err
}
return database.Set("cors:blacklist", data, expiration)
}
func (s *CacheService) GetCorsBlacklist() ([]string, error) {
data, err := database.Get("cors:blacklist")
if err != nil {
if err == redis.Nil {
return nil, nil
}
return nil, err
}
var origins []string
err = json.Unmarshal([]byte(data), &origins)
if err != nil {
return nil, err
}
return origins, nil
}
func (s *CacheService) InvalidateCorsBlacklist() error {
return database.Delete("cors:blacklist")
}
// Rate Limit Settings Cache
func (s *CacheService) SetRateLimitSettings(settings map[string]*models.RateLimitSetting, expiration time.Duration) error {
data, err := json.Marshal(settings)
if err != nil {
return err
}
return database.Set("settings:ratelimit", data, expiration)
}
func (s *CacheService) GetRateLimitSettings() (map[string]*models.RateLimitSetting, error) {
data, err := database.Get("settings:ratelimit")
if err != nil {
if err == redis.Nil {
return nil, nil
}
return nil, err
}
var settings map[string]*models.RateLimitSetting
err = json.Unmarshal([]byte(data), &settings)
if err != nil {
return nil, err
}
return settings, nil
}
func (s *CacheService) InvalidateRateLimitSettings() error {
return database.Delete("settings:ratelimit")
}

View File

@@ -0,0 +1,173 @@
package services
import (
"errors"
"fmt"
"time"
"gobeyhan/config"
"gobeyhan/database/models"
"github.com/golang-jwt/jwt/v5"
)
type JWTClaim struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Permissions []string `json:"permissions,omitempty"`
jwt.RegisteredClaims
}
type JWTService struct{}
func NewJWTService() *JWTService {
return &JWTService{}
}
func (s *JWTService) GenerateToken(user models.User) (string, error) {
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
return "", errors.New("jwt secret not configured")
}
expirationTime := time.Now().Add(24 * time.Hour)
claims := &JWTClaim{
UserID: fmt.Sprintf("%d", user.ID),
Email: user.Email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
Issuer: "gauth-central",
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.AppConfig.JWTSecret))
}
func (s *JWTService) GenerateTokenPair(user models.User) (string, string, error) {
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
return "", "", errors.New("jwt secret not configured")
}
// Extract permissions
permissionMap := make(map[string]bool)
for _, role := range user.Roles {
for _, perm := range role.Permissions {
permissionMap[perm.Name] = true
}
}
var permissions []string
for p := range permissionMap {
permissions = append(permissions, p)
}
// Access Token
expirationMinutes := 120 // Default fallback
if config.AppConfig.AccessTokenExpireMinutes > 0 {
expirationMinutes = config.AppConfig.AccessTokenExpireMinutes
}
accessTokenExp := time.Now().Add(time.Duration(expirationMinutes) * time.Minute)
accessClaims := &JWTClaim{
UserID: fmt.Sprintf("%d", user.ID),
Email: user.Email,
Permissions: permissions,
RegisteredClaims: jwt.RegisteredClaims{
Subject: fmt.Sprintf("%d", user.ID), // CRITICAL: Standard "sub" claim
ExpiresAt: jwt.NewNumericDate(accessTokenExp),
Issuer: "gauth-central",
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
signedAccessToken, err := accessToken.SignedString([]byte(config.AppConfig.JWTSecret))
if err != nil {
return "", "", err
}
// Refresh Token
expirationDays := 30 // Default fallback
if config.AppConfig.RefreshTokenExpireDays > 0 {
expirationDays = config.AppConfig.RefreshTokenExpireDays
}
refreshTokenExp := time.Now().Add(time.Duration(expirationDays) * 24 * time.Hour)
refreshClaims := &JWTClaim{
UserID: fmt.Sprintf("%d", user.ID),
Email: user.Email,
Permissions: nil, // Refresh token doesn't need permissions usually, or keep them if needed
RegisteredClaims: jwt.RegisteredClaims{
Subject: fmt.Sprintf("%d", user.ID), // CRITICAL: Standard "sub" claim
ExpiresAt: jwt.NewNumericDate(refreshTokenExp),
Issuer: "gauth-central",
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
signedRefreshToken, err := refreshToken.SignedString([]byte(config.AppConfig.JWTSecret))
if err != nil {
return "", "", err
}
return signedAccessToken, signedRefreshToken, nil
}
func (s *JWTService) ValidateToken(signedToken string) (*JWTClaim, error) {
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
return nil, errors.New("jwt secret not configured")
}
token, err := jwt.ParseWithClaims(
signedToken,
&JWTClaim{},
func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(config.AppConfig.JWTSecret), nil
},
)
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*JWTClaim)
if !ok {
return nil, errors.New("could not parse claims")
}
if claims.ExpiresAt.Time.Before(time.Now()) {
return nil, errors.New("token expired")
}
return claims, nil
}
// GenerateVerificationToken generates a JWT token for email verification (24 hours expiry)
func (s *JWTService) GenerateVerificationToken(userID, email string) (string, error) {
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
return "", errors.New("jwt secret not configured")
}
expirationTime := time.Now().Add(24 * time.Hour)
claims := &JWTClaim{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
Issuer: "gauth-central",
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.AppConfig.JWTSecret))
}
// ValidateVerificationToken validates a verification token and returns user ID and email
func (s *JWTService) ValidateVerificationToken(tokenString string) (string, string, error) {
claims, err := s.ValidateToken(tokenString)
if err != nil {
return "", "", err
}
return claims.UserID, claims.Email, nil
}

View File

@@ -0,0 +1,391 @@
package services
import (
"errors"
"gobeyhan/database"
"gobeyhan/database/models"
"log"
"net/url"
"strings"
"time"
"gorm.io/gorm"
)
type SettingsService struct {
cacheService *CacheService
}
func NewSettingsService() *SettingsService {
return &SettingsService{
cacheService: NewCacheService(),
}
}
// ==================== CORS WHITELIST ====================
func (s *SettingsService) GetAllCorsWhitelist() ([]models.CorsWhitelist, error) {
var whitelists []models.CorsWhitelist
err := database.DB.Where("is_active = ?", true).Order("created_at DESC").Find(&whitelists).Error
return whitelists, err
}
func (s *SettingsService) GetActiveWhitelistOrigins() ([]string, error) {
// Try cache first
cached, err := s.cacheService.GetCorsWhitelist()
if err == nil && cached != nil {
return cached, nil
}
origins, err := s.getActiveWhitelistOriginsFromDB()
if err != nil {
return nil, err
}
// Cache for 1 hour
s.cacheService.SetCorsWhitelist(origins, 1*time.Hour)
return origins, nil
}
var ErrCorsOriginExists = errors.New("cors origin already exists")
func (s *SettingsService) CreateCorsWhitelist(whitelist *models.CorsWhitelist) error {
var existing models.CorsWhitelist
err := database.DB.Where("LOWER(origin) = LOWER(?)", whitelist.Origin).First(&existing).Error
if err == nil {
if existing.IsActive {
return ErrCorsOriginExists
}
updates := map[string]interface{}{
"is_active": true,
"description": whitelist.Description,
"created_by": whitelist.CreatedBy,
}
err = database.DB.Model(&models.CorsWhitelist{}).Where("id = ?", existing.ID).Updates(updates).Error
if err != nil {
return err
}
s.InvalidateCorsCache()
return nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = database.DB.Create(whitelist).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
func (s *SettingsService) UpdateCorsWhitelist(id string, updates map[string]interface{}) error {
err := database.DB.Model(&models.CorsWhitelist{}).Where("id = ?", id).Updates(updates).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
func (s *SettingsService) DeleteCorsWhitelist(id string) error {
err := database.DB.Delete(&models.CorsWhitelist{}, "id = ?", id).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
func (s *SettingsService) GetCorsWhitelistByID(id uint64) (*models.CorsWhitelist, error) {
var item models.CorsWhitelist
err := database.DB.First(&item, id).Error
if err != nil {
return nil, err
}
return &item, nil
}
// ==================== CORS BLACKLIST ====================
func (s *SettingsService) GetAllCorsBlacklist() ([]models.CorsBlacklist, error) {
var blacklists []models.CorsBlacklist
err := database.DB.Where("is_active = ?", true).Order("created_at DESC").Find(&blacklists).Error
return blacklists, err
}
func (s *SettingsService) GetActiveBlacklistOrigins() ([]string, error) {
// Try cache first
cached, err := s.cacheService.GetCorsBlacklist()
if err == nil && cached != nil {
return cached, nil
}
origins, err := s.getActiveBlacklistOriginsFromDB()
if err != nil {
return nil, err
}
// Cache for 1 hour
s.cacheService.SetCorsBlacklist(origins, 1*time.Hour)
return origins, nil
}
func (s *SettingsService) CreateCorsBlacklist(blacklist *models.CorsBlacklist) error {
err := database.DB.Create(blacklist).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
func (s *SettingsService) UpdateCorsBlacklist(id string, updates map[string]interface{}) error {
err := database.DB.Model(&models.CorsBlacklist{}).Where("id = ?", id).Updates(updates).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
func (s *SettingsService) DeleteCorsBlacklist(id string) error {
err := database.DB.Delete(&models.CorsBlacklist{}, "id = ?", id).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
func (s *SettingsService) GetCorsBlacklistByID(id uint64) (*models.CorsBlacklist, error) {
var item models.CorsBlacklist
err := database.DB.First(&item, id).Error
if err != nil {
return nil, err
}
return &item, nil
}
// ==================== RATE LIMIT SETTINGS ====================
func (s *SettingsService) GetAllRateLimitSettings() ([]models.RateLimitSetting, error) {
var settings []models.RateLimitSetting
err := database.DB.Order("name ASC").Find(&settings).Error
return settings, err
}
func (s *SettingsService) GetRateLimitSettingsMap() (map[string]*models.RateLimitSetting, error) {
// Try cache first
cached, err := s.cacheService.GetRateLimitSettings()
if err == nil && cached != nil {
return cached, nil
}
// Fetch from database
var settings []models.RateLimitSetting
err = database.DB.Where("is_active = ?", true).Find(&settings).Error
if err != nil {
return nil, err
}
settingsMap := make(map[string]*models.RateLimitSetting)
for i := range settings {
settingsMap[settings[i].Name] = &settings[i]
}
// Cache for 1 hour
s.cacheService.SetRateLimitSettings(settingsMap, 1*time.Hour)
return settingsMap, nil
}
func (s *SettingsService) GetRateLimitSettingByName(name string) (*models.RateLimitSetting, error) {
settingsMap, err := s.GetRateLimitSettingsMap()
if err != nil {
return nil, err
}
setting, exists := settingsMap[name]
if !exists {
return nil, nil
}
return setting, nil
}
func (s *SettingsService) UpdateRateLimitSetting(id string, updates map[string]interface{}) error {
err := database.DB.Model(&models.RateLimitSetting{}).Where("id = ?", id).Updates(updates).Error
if err != nil {
return err
}
// Invalidate cache
s.cacheService.InvalidateRateLimitSettings()
return nil
}
func (s *SettingsService) GetRateLimitSettingByID(id uint64) (*models.RateLimitSetting, error) {
var item models.RateLimitSetting
err := database.DB.First(&item, id).Error
if err != nil {
return nil, err
}
return &item, nil
}
func (s *SettingsService) DeleteRateLimitSetting(id string) error {
err := database.DB.Delete(&models.RateLimitSetting{}, "id = ?", id).Error
if err != nil {
return err
}
// Invalidate cache
s.cacheService.InvalidateRateLimitSettings()
return nil
}
// Invalidate CORS caches (whitelist + blacklist)
func (s *SettingsService) InvalidateCorsCache() {
s.cacheService.InvalidateCorsWhitelist()
s.cacheService.InvalidateCorsBlacklist()
log.Println("cors_cache_invalidated")
}
// Check if origin is allowed
func (s *SettingsService) IsOriginAllowed(origin string) (bool, error) {
allowed, _, _, err := s.CheckOrigin(origin)
return allowed, err
}
// CheckOrigin returns decision details for debug logging.
func (s *SettingsService) CheckOrigin(origin string) (bool, string, string, error) {
// Check blacklist first
blacklist, err := s.GetActiveBlacklistOrigins()
if err != nil {
return false, "", "", err
}
for _, blocked := range blacklist {
if originMatchesEntry(origin, blocked) {
return false, blocked, "blacklist", nil
}
}
// Fallback: refresh blacklist on miss (stale cache protection)
freshBlacklist, err := s.getActiveBlacklistOriginsFromDB()
if err != nil {
return false, "", "", err
}
if len(freshBlacklist) != 0 {
s.cacheService.SetCorsBlacklist(freshBlacklist, 1*time.Hour)
}
for _, blocked := range freshBlacklist {
if originMatchesEntry(origin, blocked) {
return false, blocked, "blacklist", nil
}
}
// Check whitelist
whitelist, err := s.GetActiveWhitelistOrigins()
if err != nil {
return false, "", "", err
}
for _, allowed := range whitelist {
if allowed == "*" || originMatchesEntry(origin, allowed) {
return true, allowed, "whitelist", nil
}
}
// Fallback: refresh whitelist on miss (stale cache protection)
freshWhitelist, err := s.getActiveWhitelistOriginsFromDB()
if err != nil {
return false, "", "", err
}
if len(freshWhitelist) != 0 {
s.cacheService.SetCorsWhitelist(freshWhitelist, 1*time.Hour)
}
for _, allowed := range freshWhitelist {
if allowed == "*" || originMatchesEntry(origin, allowed) {
return true, allowed, "whitelist", nil
}
}
return false, "", "whitelist", nil
}
func (s *SettingsService) getActiveWhitelistOriginsFromDB() ([]string, error) {
var whitelists []models.CorsWhitelist
err := database.DB.Where("is_active = ?", true).Find(&whitelists).Error
if err != nil {
return nil, err
}
origins := make([]string, len(whitelists))
for i, w := range whitelists {
origins[i] = w.Origin
}
return origins, nil
}
func (s *SettingsService) getActiveBlacklistOriginsFromDB() ([]string, error) {
var blacklists []models.CorsBlacklist
err := database.DB.Where("is_active = ?", true).Find(&blacklists).Error
if err != nil {
return nil, err
}
origins := make([]string, len(blacklists))
for i, b := range blacklists {
origins[i] = b.Origin
}
return origins, nil
}
func originMatchesEntry(origin string, entry string) bool {
origin = strings.TrimSpace(origin)
entry = strings.TrimSpace(entry)
if origin == "" || entry == "" {
return false
}
originLower := strings.ToLower(origin)
entryLower := strings.ToLower(entry)
if strings.Contains(entryLower, "://") {
return originLower == entryLower
}
parsed, err := url.Parse(originLower)
if err != nil || parsed.Host == "" {
return false
}
hostLower := strings.ToLower(parsed.Host)
if entryLower == hostLower {
return true
}
// Allow entries like "127.0.0.1" to match any port
hostOnly := strings.Split(hostLower, ":")[0]
return entryLower == hostOnly
}