first commit
This commit is contained in:
361
app/account/handlers/auth_handler.go
Normal file
361
app/account/handlers/auth_handler.go
Normal 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
|
||||
}
|
||||
297
app/account/handlers/oauth_handler.go
Normal file
297
app/account/handlers/oauth_handler.go
Normal 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
|
||||
}
|
||||
70
app/account/handlers/permission_handler.go
Normal file
70
app/account/handlers/permission_handler.go
Normal 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})
|
||||
}
|
||||
169
app/account/handlers/role_handler.go
Normal file
169
app/account/handlers/role_handler.go
Normal 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"})
|
||||
}
|
||||
111
app/account/handlers/social_account_handler.go
Normal file
111
app/account/handlers/social_account_handler.go
Normal 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"})
|
||||
}
|
||||
287
app/account/handlers/user_handler.go
Normal file
287
app/account/handlers/user_handler.go
Normal 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"})
|
||||
}
|
||||
42
app/account/services/permission_service.go
Normal file
42
app/account/services/permission_service.go
Normal 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
|
||||
}
|
||||
96
app/account/services/role_service.go
Normal file
96
app/account/services/role_service.go
Normal 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)
|
||||
}
|
||||
54
app/account/services/social_account_service.go
Normal file
54
app/account/services/social_account_service.go
Normal 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
|
||||
}
|
||||
184
app/account/services/user_service.go
Normal file
184
app/account/services/user_service.go
Normal 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)
|
||||
}
|
||||
235
app/blog/handlers/category_handler.go
Normal file
235
app/blog/handlers/category_handler.go
Normal 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"})
|
||||
}
|
||||
111
app/blog/handlers/category_view_handler.go
Normal file
111
app/blog/handlers/category_view_handler.go
Normal 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,
|
||||
})
|
||||
}
|
||||
245
app/blog/handlers/comment_handler.go
Normal file
245
app/blog/handlers/comment_handler.go
Normal 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"})
|
||||
}
|
||||
306
app/blog/handlers/post_handler.go
Normal file
306
app/blog/handlers/post_handler.go
Normal 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"})
|
||||
}
|
||||
220
app/blog/handlers/tag_handler.go
Normal file
220
app/blog/handlers/tag_handler.go
Normal 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"})
|
||||
}
|
||||
107
app/blog/services/category_service.go
Normal file
107
app/blog/services/category_service.go
Normal 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
|
||||
}
|
||||
67
app/blog/services/category_view_service.go
Normal file
67
app/blog/services/category_view_service.go
Normal 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
|
||||
}
|
||||
115
app/blog/services/comment_service.go
Normal file
115
app/blog/services/comment_service.go
Normal 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
|
||||
}
|
||||
202
app/blog/services/post_service.go
Normal file
202
app/blog/services/post_service.go
Normal 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
|
||||
}
|
||||
87
app/blog/services/tag_service.go
Normal file
87
app/blog/services/tag_service.go
Normal 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
|
||||
}
|
||||
49
app/middlewares/admin_middleware.go
Normal file
49
app/middlewares/admin_middleware.go
Normal 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()
|
||||
}
|
||||
}
|
||||
47
app/middlewares/auth_middleware.go
Normal file
47
app/middlewares/auth_middleware.go
Normal 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()
|
||||
}
|
||||
}
|
||||
57
app/middlewares/dynamic_cors_middleware.go
Normal file
57
app/middlewares/dynamic_cors_middleware.go
Normal 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()
|
||||
}
|
||||
}
|
||||
200
app/middlewares/rate_limit_middleware.go
Normal file
200
app/middlewares/rate_limit_middleware.go
Normal 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
302
app/routes/routes.go
Normal 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
462
app/routes/routes.go.backup
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
264
app/settings/handlers/settings_handler.go
Normal file
264
app/settings/handlers/settings_handler.go
Normal 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"})
|
||||
}
|
||||
204
app/settings/services/cache_service.go
Normal file
204
app/settings/services/cache_service.go
Normal 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")
|
||||
}
|
||||
173
app/settings/services/jwt_service.go
Normal file
173
app/settings/services/jwt_service.go
Normal 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
|
||||
}
|
||||
391
app/settings/services/settings_service.go
Normal file
391
app/settings/services/settings_service.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user