first commit
This commit is contained in:
267
api/handlers/auth_handler.go
Normal file
267
api/handlers/auth_handler.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/markbates/goth/gothic"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
|
||||
return &AuthHandler{authService: authService}
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
UserName string `json:"username" binding:"required,min=3"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type RefreshRequest struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
}
|
||||
|
||||
// Register godoc
|
||||
// @Summary Register a new user
|
||||
// @Description Register with username, email and password
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RegisterRequest true "Register Request"
|
||||
// @Success 201 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /auth/register [post]
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Register creates user with email_verified=false; tokens only after email verification
|
||||
user, _, _, verifyToken, err := h.authService.Register(req.UserName, req.Email, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Send verification email asynchronously
|
||||
go func() {
|
||||
if err := utils.SendVerificationEmail(user.Email, verifyToken); err != nil {
|
||||
fmt.Printf("Failed to send verification email to %s: %v\n", user.Email, err)
|
||||
} else {
|
||||
fmt.Printf("Verification email sent to %s\n", user.Email)
|
||||
}
|
||||
}()
|
||||
|
||||
roles := user.Roles
|
||||
if roles == nil {
|
||||
roles = []models.Role{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "User created. Please verify your email.",
|
||||
"user_id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"roles": roles,
|
||||
"email_verified": false,
|
||||
"verification_token": verifyToken, // Returned for dev convenience, usually hidden in prod
|
||||
})
|
||||
}
|
||||
|
||||
// Login godoc
|
||||
// @Summary Login user
|
||||
// @Description Login with email and password to get JWT token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body LoginRequest true "Login Request"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/login [post]
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, accessToken, refreshToken, err := h.authService.Login(req.Email, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure roles is always returned, even if empty
|
||||
roles := user.Roles
|
||||
if roles == nil {
|
||||
roles = []models.Role{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user_id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"roles": roles,
|
||||
"access_token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
// BeginAuth godoc
|
||||
// @Summary Start OAuth2 flow
|
||||
// @Description Redirect to OAuth2 provider
|
||||
// @Tags oauth
|
||||
// @Param provider path string true "Provider (google, github)"
|
||||
// @Router /auth/{provider} [get]
|
||||
func (h *AuthHandler) BeginAuth(c *gin.Context) {
|
||||
// Try to complete user auth if we've already got a session
|
||||
// but context is not set correctly for gin with gothic usually
|
||||
provider := c.Param("provider")
|
||||
q := c.Request.URL.Query()
|
||||
q.Add("provider", provider)
|
||||
c.Request.URL.RawQuery = q.Encode()
|
||||
|
||||
gothic.BeginAuthHandler(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
// Callback godoc
|
||||
// @Summary OAuth2 Callback
|
||||
// @Description Handle callback from OAuth2 provider
|
||||
// @Tags oauth
|
||||
// @Param provider path string true "Provider (google, github)"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/{provider}/callback [get]
|
||||
func (h *AuthHandler) Callback(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
q := c.Request.URL.Query()
|
||||
q.Add("provider", provider)
|
||||
c.Request.URL.RawQuery = q.Encode()
|
||||
|
||||
gothUser, err := gothic.CompleteUserAuth(c.Writer, c.Request)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, accessToken, refreshToken, err := h.authService.FindOrCreateSocialUser(gothUser, provider)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure roles is always returned
|
||||
roles := user.Roles
|
||||
if roles == nil {
|
||||
roles = []models.Role{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"access_token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"email_verified": user.EmailVerified,
|
||||
"roles": roles,
|
||||
"social_accounts": user.SocialAccounts,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Refresh godoc
|
||||
// @Summary Refresh Access Token
|
||||
// @Description usage: send refresh_token to get new access_token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RefreshRequest true "Refresh Request"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *AuthHandler) Refresh(c *gin.Context) {
|
||||
var req RefreshRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, refreshToken, err := h.authService.RefreshToken(req.RefreshToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"access_token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyEmail godoc
|
||||
// @Summary Verify email address
|
||||
// @Description Verify email with token sent after email/password registration
|
||||
// @Tags auth
|
||||
// @Param token query string true "Verification token"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /auth/verify-email [get]
|
||||
func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"})
|
||||
return
|
||||
}
|
||||
if err := h.authService.VerifyEmail(token); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Email verified successfully"})
|
||||
}
|
||||
|
||||
// Me godoc
|
||||
// @Summary Get Current User Profile
|
||||
// @Description Get details of the currently authenticated user
|
||||
// @Tags auth
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/me [get]
|
||||
func (h *AuthHandler) Me(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.authService.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
193
api/handlers/avatar_handler.go
Normal file
193
api/handlers/avatar_handler.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/database"
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AvatarHandler struct{}
|
||||
|
||||
func NewAvatarHandler() *AvatarHandler {
|
||||
return &AvatarHandler{}
|
||||
}
|
||||
|
||||
// UploadAvatar godoc
|
||||
// @Summary Upload user avatar
|
||||
// @Tags User
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param avatar formData file true "Avatar image file"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /user/avatar [post]
|
||||
func (h *AvatarHandler) UploadAvatar(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse multipart form
|
||||
file, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user to check for old avatar
|
||||
var user models.User
|
||||
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete old avatar file if exists and is not from OAuth
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") {
|
||||
oldPath := "." + user.Avatar
|
||||
os.Remove(oldPath) // Ignore error if file doesn't exist
|
||||
}
|
||||
|
||||
// Use utils.SaveOptimizedImage
|
||||
avatarURL, err := utils.SaveOptimizedImage(file, "./uploads/avatars", userID, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update avatar URL
|
||||
if err := database.DB.Model(&user).Update("avatar", avatarURL).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update avatar"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Avatar uploaded successfully",
|
||||
"avatar_url": avatarURL,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": avatarURL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAvatar godoc
|
||||
// @Summary Delete user avatar
|
||||
// @Tags User
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /user/avatar [delete]
|
||||
func (h *AvatarHandler) DeleteAvatar(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete avatar file if it's a local upload
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") {
|
||||
filepath := "." + user.Avatar
|
||||
os.Remove(filepath) // Ignore error if file doesn't exist
|
||||
}
|
||||
|
||||
// Set avatar to empty string
|
||||
if err := database.DB.Model(&user).Update("avatar", "").Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete avatar"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Avatar deleted successfully",
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// AdminUploadAvatar godoc
|
||||
// @Summary Upload avatar for any user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Param avatar formData file true "Avatar image file"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id}/avatar [post]
|
||||
func (h *AvatarHandler) AdminUploadAvatar(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
// Parse multipart form
|
||||
file, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
if file.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user to check for old avatar
|
||||
var user models.User
|
||||
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete old avatar file if exists and is not from OAuth
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") {
|
||||
oldPath := "." + user.Avatar
|
||||
os.Remove(oldPath) // Ignore error if file doesn't exist
|
||||
}
|
||||
|
||||
// Use utils.SaveOptimizedImage
|
||||
avatarURL, err := utils.SaveOptimizedImage(file, "./uploads/avatars", userID, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update avatar URL
|
||||
if err := database.DB.Model(&user).Update("avatar", avatarURL).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update avatar"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Avatar uploaded successfully",
|
||||
"avatar_url": avatarURL,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"avatar": avatarURL,
|
||||
},
|
||||
})
|
||||
}
|
||||
326
api/handlers/profile_handler.go
Normal file
326
api/handlers/profile_handler.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gauth-central/internal/database"
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type ProfileHandler struct{}
|
||||
|
||||
func NewProfileHandler() *ProfileHandler {
|
||||
return &ProfileHandler{}
|
||||
}
|
||||
|
||||
// isOAuthUser checks if user is an OAuth user (has social accounts)
|
||||
func isOAuthUser(user *models.User) bool {
|
||||
// OAuth user if they have social accounts OR if they don't have a password
|
||||
return len(user.SocialAccounts) > 0 || user.Password == ""
|
||||
}
|
||||
|
||||
// GetProfile godoc
|
||||
// @Summary Get current user profile
|
||||
// @Tags Profile
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Router /profile [get]
|
||||
func (h *ProfileHandler) GetProfile(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.
|
||||
Preload("Roles").
|
||||
Preload("Roles.Permissions").
|
||||
Preload("SocialAccounts").
|
||||
Where("id = ?", userID).
|
||||
First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Add is_oauth_user flag to response
|
||||
type ProfileResponse struct {
|
||||
models.User
|
||||
IsOAuthUser bool `json:"is_oauth_user"`
|
||||
}
|
||||
|
||||
response := ProfileResponse{
|
||||
User: user,
|
||||
IsOAuthUser: isOAuthUser(&user),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// UpdateProfile godoc
|
||||
// @Summary Update current user profile
|
||||
// @Tags Profile
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param user_name formData string false "Username"
|
||||
// @Param avatar formData file false "Avatar image"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /profile [put]
|
||||
func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Try to parse as multipart form first
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
isMultipart := strings.Contains(contentType, "multipart/form-data")
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
if isMultipart {
|
||||
// Parse multipart form
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse form data"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get form values
|
||||
if userName := c.PostForm("user_name"); userName != "" {
|
||||
updates["user_name"] = userName
|
||||
}
|
||||
} else {
|
||||
// Parse as JSON
|
||||
var input struct {
|
||||
UserName *string `json:"user_name"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if input.UserName != nil {
|
||||
updates["user_name"] = *input.UserName
|
||||
}
|
||||
}
|
||||
|
||||
// Update basic user fields
|
||||
if len(updates) > 0 {
|
||||
if err := database.DB.Model(&models.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle avatar upload if multipart and file provided
|
||||
if isMultipart {
|
||||
avatarFile, err := c.FormFile("avatar")
|
||||
if err == nil && avatarFile != nil {
|
||||
// Validate file size (max 5MB)
|
||||
if avatarFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Avatar file size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user to check for old avatar
|
||||
var user models.User
|
||||
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete old avatar if exists and is local
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") {
|
||||
oldPath := "." + user.Avatar
|
||||
os.Remove(oldPath)
|
||||
}
|
||||
|
||||
// Use utils.SaveOptimizedImage
|
||||
avatarURL, err := utils.SaveOptimizedImage(avatarFile, "./uploads/avatars", userID, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update user avatar
|
||||
if err := database.DB.Model(&user).Update("avatar", avatarURL).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update avatar in database"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get updated user to return
|
||||
var user models.User
|
||||
if err := database.DB.
|
||||
Preload("Roles").
|
||||
Preload("SocialAccounts").
|
||||
Where("id = ?", userID).
|
||||
First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Profile updated successfully"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Profile updated successfully",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangePassword godoc
|
||||
// @Summary Change password
|
||||
// @Tags Profile
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body object true "Password change request"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /profile/password [put]
|
||||
func (h *ProfileHandler) ChangePassword(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
CurrentPassword string `json:"current_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user
|
||||
var user models.User
|
||||
if err := database.DB.Preload("SocialAccounts").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is OAuth user
|
||||
if isOAuthUser(&user) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change password for OAuth users (Google/GitHub login)"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.CurrentPassword)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Current password is incorrect"})
|
||||
return
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update password
|
||||
if err := database.DB.Model(&user).Update("password", string(hashedPassword)).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update password"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Password changed successfully"})
|
||||
}
|
||||
|
||||
// ChangeEmail godoc
|
||||
// @Summary Change email address
|
||||
// @Tags Profile
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body object true "Email change request"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /profile/email [put]
|
||||
func (h *ProfileHandler) ChangeEmail(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
NewEmail string `json:"new_email" binding:"required,email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user
|
||||
var user models.User
|
||||
if err := database.DB.Preload("SocialAccounts").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is OAuth user
|
||||
if isOAuthUser(&user) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change email for OAuth users (Google/GitHub login)"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Password is incorrect"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if new email already exists
|
||||
var existingUser models.User
|
||||
if err := database.DB.Where("email = ? AND id != ?", input.NewEmail, userID).First(&existingUser).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Email already in use"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate verification token
|
||||
verifyToken, err := utils.GenerateSecureToken(32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate verification token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update email and set as unverified
|
||||
falseBool := false
|
||||
updates := map[string]interface{}{
|
||||
"email": input.NewEmail,
|
||||
"email_verified": &falseBool,
|
||||
"email_verify_token": verifyToken,
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&user).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update email"})
|
||||
return
|
||||
}
|
||||
|
||||
// Send verification email
|
||||
go func() {
|
||||
if err := utils.SendVerificationEmail(input.NewEmail, verifyToken); err != nil {
|
||||
fmt.Printf("Failed to send verification email to %s: %v\n", input.NewEmail, err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Email updated. Please verify your new email address.",
|
||||
"new_email": input.NewEmail,
|
||||
"verification_token": verifyToken,
|
||||
})
|
||||
}
|
||||
328
api/handlers/settings_handler.go
Normal file
328
api/handlers/settings_handler.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SettingsHandler struct {
|
||||
settingsService *services.SettingsService
|
||||
}
|
||||
|
||||
func NewSettingsHandler(settingsService *services.SettingsService) *SettingsHandler {
|
||||
return &SettingsHandler{
|
||||
settingsService: settingsService,
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CORS WHITELIST ====================
|
||||
|
||||
// GetAllWhitelist godoc
|
||||
// @Summary Get all CORS whitelist entries
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.CorsWhitelist
|
||||
// @Router /settings/cors/whitelist [get]
|
||||
func (h *SettingsHandler) GetAllWhitelist(c *gin.Context) {
|
||||
whitelists, err := h.settingsService.GetAllCorsWhitelist()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch whitelist"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, whitelists)
|
||||
}
|
||||
|
||||
// CreateWhitelist godoc
|
||||
// @Summary Create CORS whitelist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param whitelist body object true "Whitelist data"
|
||||
// @Success 201 {object} models.CorsWhitelist
|
||||
// @Router /settings/cors/whitelist [post]
|
||||
func (h *SettingsHandler) CreateWhitelist(c *gin.Context) {
|
||||
var input struct {
|
||||
Origin string `json:"origin" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
email := c.GetString("email")
|
||||
whitelist := &models.CorsWhitelist{
|
||||
Origin: input.Origin,
|
||||
Description: input.Description,
|
||||
IsActive: true,
|
||||
CreatedBy: email,
|
||||
}
|
||||
|
||||
err := h.settingsService.CreateCorsWhitelist(whitelist)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create whitelist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, whitelist)
|
||||
}
|
||||
|
||||
// UpdateWhitelist godoc
|
||||
// @Summary Update CORS whitelist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Whitelist ID"
|
||||
// @Param whitelist body object true "Update data"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/cors/whitelist/{id} [put]
|
||||
func (h *SettingsHandler) UpdateWhitelist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var input struct {
|
||||
Origin *string `json:"origin"`
|
||||
Description *string `json:"description"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if input.Origin != nil {
|
||||
updates["origin"] = *input.Origin
|
||||
}
|
||||
if input.Description != nil {
|
||||
updates["description"] = *input.Description
|
||||
}
|
||||
if input.IsActive != nil {
|
||||
updates["is_active"] = *input.IsActive
|
||||
}
|
||||
|
||||
err := h.settingsService.UpdateCorsWhitelist(id, updates)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update whitelist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Whitelist updated successfully"})
|
||||
}
|
||||
|
||||
// DeleteWhitelist godoc
|
||||
// @Summary Delete CORS whitelist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Whitelist ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/cors/whitelist/{id} [delete]
|
||||
func (h *SettingsHandler) DeleteWhitelist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
err := h.settingsService.DeleteCorsWhitelist(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete whitelist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Whitelist entry deleted successfully"})
|
||||
}
|
||||
|
||||
// ==================== CORS BLACKLIST ====================
|
||||
|
||||
// GetAllBlacklist godoc
|
||||
// @Summary Get all CORS blacklist entries
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.CorsBlacklist
|
||||
// @Router /settings/cors/blacklist [get]
|
||||
func (h *SettingsHandler) GetAllBlacklist(c *gin.Context) {
|
||||
blacklists, err := h.settingsService.GetAllCorsBlacklist()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch blacklist"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, blacklists)
|
||||
}
|
||||
|
||||
// CreateBlacklist godoc
|
||||
// @Summary Create CORS blacklist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param blacklist body object true "Blacklist data"
|
||||
// @Success 201 {object} models.CorsBlacklist
|
||||
// @Router /settings/cors/blacklist [post]
|
||||
func (h *SettingsHandler) CreateBlacklist(c *gin.Context) {
|
||||
var input struct {
|
||||
Origin string `json:"origin" binding:"required"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
email := c.GetString("email")
|
||||
blacklist := &models.CorsBlacklist{
|
||||
Origin: input.Origin,
|
||||
Reason: input.Reason,
|
||||
IsActive: true,
|
||||
CreatedBy: email,
|
||||
}
|
||||
|
||||
err := h.settingsService.CreateCorsBlacklist(blacklist)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create blacklist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, blacklist)
|
||||
}
|
||||
|
||||
// UpdateBlacklist godoc
|
||||
// @Summary Update CORS blacklist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Blacklist ID"
|
||||
// @Param blacklist body object true "Update data"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/cors/blacklist/{id} [put]
|
||||
func (h *SettingsHandler) UpdateBlacklist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var input struct {
|
||||
Origin *string `json:"origin"`
|
||||
Reason *string `json:"reason"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if input.Origin != nil {
|
||||
updates["origin"] = *input.Origin
|
||||
}
|
||||
if input.Reason != nil {
|
||||
updates["reason"] = *input.Reason
|
||||
}
|
||||
if input.IsActive != nil {
|
||||
updates["is_active"] = *input.IsActive
|
||||
}
|
||||
|
||||
err := h.settingsService.UpdateCorsBlacklist(id, updates)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update blacklist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Blacklist updated successfully"})
|
||||
}
|
||||
|
||||
// DeleteBlacklist godoc
|
||||
// @Summary Delete CORS blacklist entry
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "Blacklist ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/cors/blacklist/{id} [delete]
|
||||
func (h *SettingsHandler) DeleteBlacklist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
err := h.settingsService.DeleteCorsBlacklist(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete blacklist entry"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Blacklist entry deleted successfully"})
|
||||
}
|
||||
|
||||
// ==================== RATE LIMIT SETTINGS ====================
|
||||
|
||||
// GetAllRateLimits godoc
|
||||
// @Summary Get all rate limit settings
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.RateLimitSetting
|
||||
// @Router /settings/ratelimit [get]
|
||||
func (h *SettingsHandler) GetAllRateLimits(c *gin.Context) {
|
||||
settings, err := h.settingsService.GetAllRateLimitSettings()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch rate limit settings"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, settings)
|
||||
}
|
||||
|
||||
// UpdateRateLimit godoc
|
||||
// @Summary Update rate limit setting
|
||||
// @Tags Settings
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Setting ID"
|
||||
// @Param setting body object true "Update data"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /settings/ratelimit/{id} [put]
|
||||
func (h *SettingsHandler) UpdateRateLimit(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var input struct {
|
||||
MaxRequests *int64 `json:"max_requests"`
|
||||
WindowSeconds *int `json:"window_seconds"`
|
||||
Description *string `json:"description"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
email := c.GetString("email")
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
if input.MaxRequests != nil {
|
||||
updates["max_requests"] = *input.MaxRequests
|
||||
}
|
||||
if input.WindowSeconds != nil {
|
||||
updates["window_seconds"] = *input.WindowSeconds
|
||||
}
|
||||
if input.Description != nil {
|
||||
updates["description"] = *input.Description
|
||||
}
|
||||
if input.IsActive != nil {
|
||||
updates["is_active"] = *input.IsActive
|
||||
}
|
||||
updates["updated_by"] = email
|
||||
|
||||
err := h.settingsService.UpdateRateLimitSetting(id, updates)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update rate limit setting"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Rate limit setting updated successfully"})
|
||||
}
|
||||
538
api/handlers/user_management_handler.go
Normal file
538
api/handlers/user_management_handler.go
Normal file
@@ -0,0 +1,538 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gauth-central/internal/database"
|
||||
"gauth-central/internal/models"
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UserManagementHandler struct {
|
||||
userService *services.UserManagementService
|
||||
}
|
||||
|
||||
func NewUserManagementHandler(userService *services.UserManagementService) *UserManagementHandler {
|
||||
return &UserManagementHandler{
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllUsers godoc
|
||||
// @Summary Get all users (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param limit query int false "Items per page" default(10)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users [get]
|
||||
func (h *UserManagementHandler) GetAllUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
users, total, err := h.userService.GetAllUsers(page, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": users,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"totalPages": (total + int64(limit) - 1) / int64(limit),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserByID godoc
|
||||
// @Summary Get user by ID (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} models.User
|
||||
// @Router /admin/users/{id} [get]
|
||||
func (h *UserManagementHandler) GetUserByID(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
user, err := h.userService.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// CreateUser godoc
|
||||
// @Summary Create new user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param email formData string true "Email"
|
||||
// @Param password formData string true "Password"
|
||||
// @Param user_name formData string true "Username"
|
||||
// @Param email_verified formData boolean false "Email verified"
|
||||
// @Param roles formData string false "Roles (comma separated: admin,user)"
|
||||
// @Param avatar formData file false "Avatar image"
|
||||
// @Success 201 {object} models.User
|
||||
// @Router /admin/users [post]
|
||||
func (h *UserManagementHandler) CreateUser(c *gin.Context) {
|
||||
// Parse multipart form
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse form data"})
|
||||
return
|
||||
}
|
||||
|
||||
email := c.PostForm("email")
|
||||
password := c.PostForm("password")
|
||||
userName := c.PostForm("user_name")
|
||||
emailVerified := c.PostForm("email_verified") == "true"
|
||||
rolesStr := c.PostForm("roles")
|
||||
|
||||
// Validate required fields
|
||||
if email == "" || password == "" || userName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "email, password, and user_name are required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse roles
|
||||
var roles []string
|
||||
if rolesStr != "" {
|
||||
roles = strings.Split(rolesStr, ",")
|
||||
// Trim spaces
|
||||
for i, role := range roles {
|
||||
roles[i] = strings.TrimSpace(role)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := h.userService.CreateUser(
|
||||
email,
|
||||
password,
|
||||
userName,
|
||||
emailVerified,
|
||||
roles,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Handle avatar upload if provided
|
||||
avatarFile, err := c.FormFile("avatar")
|
||||
if err == nil && avatarFile != nil {
|
||||
// Validate file size (max 5MB)
|
||||
if avatarFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Avatar file size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Use utils.SaveOptimizedImage
|
||||
// Default options (WebP, 800px width)
|
||||
avatarURL, err := utils.SaveOptimizedImage(avatarFile, "./uploads/avatars", user.ID.String(), nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update user avatar
|
||||
database.DB.Model(&user).Update("avatar", avatarURL)
|
||||
user.Avatar = avatarURL
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, user)
|
||||
}
|
||||
|
||||
// UpdateUser godoc
|
||||
// @Summary Update user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Param email formData string false "Email"
|
||||
// @Param password formData string false "Password"
|
||||
// @Param user_name formData string false "Username"
|
||||
// @Param email_verified formData boolean false "Email verified"
|
||||
// @Param roles formData string false "Roles (comma separated: admin,user)"
|
||||
// @Param avatar formData file false "Avatar image"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id} [put]
|
||||
func (h *UserManagementHandler) UpdateUser(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
// Try to parse as multipart form first
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
isMultipart := strings.Contains(contentType, "multipart/form-data")
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
var roles []string
|
||||
|
||||
if isMultipart {
|
||||
// Parse multipart form
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse form data"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get form values
|
||||
if email := c.PostForm("email"); email != "" {
|
||||
updates["email"] = email
|
||||
}
|
||||
if password := c.PostForm("password"); password != "" {
|
||||
updates["password"] = password
|
||||
}
|
||||
if userName := c.PostForm("user_name"); userName != "" {
|
||||
updates["user_name"] = userName
|
||||
}
|
||||
if emailVerified := c.PostForm("email_verified"); emailVerified != "" {
|
||||
updates["email_verified"] = emailVerified == "true"
|
||||
}
|
||||
if rolesStr := c.PostForm("roles"); rolesStr != "" {
|
||||
roles = strings.Split(rolesStr, ",")
|
||||
for i, role := range roles {
|
||||
roles[i] = strings.TrimSpace(role)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Parse as JSON
|
||||
var input struct {
|
||||
Email *string `json:"email"`
|
||||
Password *string `json:"password"`
|
||||
UserName *string `json:"user_name"`
|
||||
Avatar *string `json:"avatar"`
|
||||
EmailVerified *bool `json:"email_verified"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if input.Email != nil {
|
||||
updates["email"] = *input.Email
|
||||
}
|
||||
if input.Password != nil {
|
||||
updates["password"] = *input.Password
|
||||
}
|
||||
if input.UserName != nil {
|
||||
updates["user_name"] = *input.UserName
|
||||
}
|
||||
if input.Avatar != nil {
|
||||
updates["avatar"] = *input.Avatar
|
||||
}
|
||||
if input.EmailVerified != nil {
|
||||
updates["email_verified"] = *input.EmailVerified
|
||||
}
|
||||
if input.Roles != nil {
|
||||
roles = input.Roles
|
||||
}
|
||||
}
|
||||
|
||||
// Update basic user fields
|
||||
if len(updates) > 0 {
|
||||
if err := h.userService.UpdateUser(userID, updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Update roles if provided
|
||||
if len(roles) > 0 {
|
||||
if err := h.userService.AssignRoles(userID, roles); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update roles: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle avatar upload if multipart and file provided
|
||||
if isMultipart {
|
||||
avatarFile, err := c.FormFile("avatar")
|
||||
if err == nil && avatarFile != nil {
|
||||
// Validate file size (max 5MB)
|
||||
if avatarFile.Size > 5*1024*1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Avatar file size exceeds 5MB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user to check for old avatar
|
||||
var user models.User
|
||||
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete old avatar if exists and is local
|
||||
if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") {
|
||||
oldPath := "." + user.Avatar
|
||||
os.Remove(oldPath)
|
||||
}
|
||||
|
||||
// Use utils.SaveOptimizedImage
|
||||
avatarURL, err := utils.SaveOptimizedImage(avatarFile, "./uploads/avatars", userID, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update user avatar
|
||||
if err := database.DB.Model(&user).Update("avatar", avatarURL).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update avatar in database"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get updated user to return
|
||||
user, err := h.userService.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User updated successfully"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "User updated successfully",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteUser godoc
|
||||
// @Summary Delete user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param hard query boolean false "Hard delete (permanent)" default(false)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id} [delete]
|
||||
func (h *UserManagementHandler) DeleteUser(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
hardDelete := c.Query("hard") == "true"
|
||||
|
||||
// Prevent deleting self
|
||||
currentUserID := c.GetString("user_id")
|
||||
if userID == currentUserID {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete your own account"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userService.DeleteUser(userID, hardDelete); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"})
|
||||
return
|
||||
}
|
||||
|
||||
deleteType := "soft"
|
||||
if hardDelete {
|
||||
deleteType = "permanently"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User deleted " + deleteType + " successfully"})
|
||||
}
|
||||
|
||||
// AssignRoles godoc
|
||||
// @Summary Assign roles to user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Param roles body object true "Roles"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id}/roles [post]
|
||||
func (h *UserManagementHandler) AssignRoles(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
var input struct {
|
||||
Roles []string `json:"roles" binding:"required"` // ["admin", "user"]
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userService.AssignRoles(userID, input.Roles); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to assign roles: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Roles assigned successfully"})
|
||||
}
|
||||
|
||||
// RemoveRole godoc
|
||||
// @Summary Remove role from user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param role path string true "Role name"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id}/roles/{role} [delete]
|
||||
func (h *UserManagementHandler) RemoveRole(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
roleName := c.Param("role")
|
||||
|
||||
if err := h.userService.RemoveRole(userID, roleName); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove role"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Role removed successfully"})
|
||||
}
|
||||
|
||||
// SearchUsers godoc
|
||||
// @Summary Search users (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param q query string true "Search query"
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param limit query int false "Items per page" default(10)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/search [get]
|
||||
func (h *UserManagementHandler) SearchUsers(c *gin.Context) {
|
||||
query := c.Query("q")
|
||||
if query == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Search query required"})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
users, total, err := h.userService.SearchUsers(query, page, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search users"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": users,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"totalPages": (total + int64(limit) - 1) / int64(limit),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetDeletedUsers godoc
|
||||
// @Summary Get all soft deleted users (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param limit query int false "Items per page" default(10)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/deleted [get]
|
||||
func (h *UserManagementHandler) GetDeletedUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
users, total, err := h.userService.GetDeletedUsers(page, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch deleted users"})
|
||||
return
|
||||
}
|
||||
|
||||
// Transform users to include deleted_at field
|
||||
type DeletedUserResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserName string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deleted_at"`
|
||||
Roles []models.Role `json:"roles,omitempty"`
|
||||
SocialAccounts []models.SocialAccount `json:"social_accounts,omitempty"`
|
||||
}
|
||||
|
||||
deletedUsers := make([]DeletedUserResponse, len(users))
|
||||
for i, user := range users {
|
||||
var deletedAt *time.Time
|
||||
if user.DeletedAt.Valid {
|
||||
deletedAt = &user.DeletedAt.Time
|
||||
}
|
||||
|
||||
emailVerified := false
|
||||
if user.EmailVerified != nil {
|
||||
emailVerified = *user.EmailVerified
|
||||
}
|
||||
|
||||
deletedUsers[i] = DeletedUserResponse{
|
||||
ID: user.ID.String(),
|
||||
UserName: user.UserName,
|
||||
Email: user.Email,
|
||||
Avatar: user.Avatar,
|
||||
EmailVerified: emailVerified,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
DeletedAt: deletedAt,
|
||||
Roles: user.Roles,
|
||||
SocialAccounts: user.SocialAccounts,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": deletedUsers,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"totalPages": (total + int64(limit) - 1) / int64(limit),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// RestoreUser godoc
|
||||
// @Summary Restore a soft deleted user (Admin only)
|
||||
// @Tags Admin - User Management
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /admin/users/{id}/restore [post]
|
||||
func (h *UserManagementHandler) RestoreUser(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
if err := h.userService.RestoreUser(userID); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User restored successfully"})
|
||||
}
|
||||
49
api/middlewares/admin_middleware.go
Normal file
49
api/middlewares/admin_middleware.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gauth-central/internal/database"
|
||||
"gauth-central/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AdminMiddleware - Sadece admin rolündeki kullanıcıların erişimini sağlar
|
||||
func AdminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Get user_id from context (set by AuthMiddleware)
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch user with roles
|
||||
var user models.User
|
||||
err := database.DB.Preload("Roles").Where("id = ?", userID).First(&user).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user has admin role
|
||||
hasAdminRole := false
|
||||
for _, role := range user.Roles {
|
||||
if role.Name == "admin" {
|
||||
hasAdminRole = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAdminRole {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
30
api/middlewares/auth_middleware.go
Normal file
30
api/middlewares/auth_middleware.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"gauth-central/internal/services"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AuthMiddleware(jwtService *services.JWTService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
|
||||
claims, err := jwtService.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("email", claims.Email)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
53
api/middlewares/dynamic_cors_middleware.go
Normal file
53
api/middlewares/dynamic_cors_middleware.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"gauth-central/internal/services"
|
||||
"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
|
||||
}
|
||||
|
||||
// Check if origin is allowed
|
||||
allowed, err := settingsService.IsOriginAllowed(origin)
|
||||
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
api/middlewares/rate_limit_middleware.go
Normal file
200
api/middlewares/rate_limit_middleware.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gauth-central/internal/services"
|
||||
"gauth-central/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RateLimitMiddleware creates a rate limiting middleware
|
||||
func RateLimitMiddleware(maxRequests int64, duration time.Duration) gin.HandlerFunc {
|
||||
cacheService := services.NewCacheService()
|
||||
settingsService := services.NewSettingsService()
|
||||
|
||||
return func(c *gin.Context) {
|
||||
// Get client IP
|
||||
clientIP := c.ClientIP()
|
||||
path := c.Request.URL.Path
|
||||
method := c.Request.Method
|
||||
|
||||
// Skip checks for localhost (hardcoded safety)
|
||||
if clientIP == "::1" || clientIP == "127.0.0.1" || clientIP == "localhost" {
|
||||
fmt.Printf("%s[LOCALHOST BYPASS]%s IP: %s accessed %s %s\n", utils.ColorCyan, utils.ColorReset, clientIP, method, path)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Check Blacklist from DB
|
||||
blacklist, err := settingsService.GetActiveBlacklistOrigins()
|
||||
if err == nil {
|
||||
for _, blocked := range blacklist {
|
||||
if blocked == clientIP || strings.Contains(blocked, clientIP) {
|
||||
fmt.Printf("%s[BLACKLIST BLOCKED]%s IP: %s tried to access %s %s\n", utils.ColorRed, utils.ColorReset, clientIP, method, path)
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Access denied. Your IP is blacklisted.",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check Whitelist from DB (Skip Rate Limit)
|
||||
whitelist, err := settingsService.GetActiveWhitelistOrigins()
|
||||
if err == nil {
|
||||
for _, allowed := range whitelist {
|
||||
if allowed == clientIP || strings.Contains(allowed, clientIP) {
|
||||
fmt.Printf("%s[WHITELIST ALLOWED]%s IP: %s accessed %s %s (Rate Limit Skipped)\n", utils.ColorGreen, utils.ColorReset, clientIP, method, path)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key := clientIP
|
||||
|
||||
// Increment counter
|
||||
count, err := cacheService.IncrementRateLimit(key, duration)
|
||||
if err != nil {
|
||||
// If Redis is down, allow the request but log error
|
||||
fmt.Printf("%s[REDIS ERROR]%s Could not increment rate limit for %s: %v\n", utils.ColorRed, utils.ColorReset, clientIP, err)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
remaining := maxRequests - count
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
|
||||
// Check if limit exceeded
|
||||
if count > maxRequests {
|
||||
fmt.Printf("%s[RATE LIMIT EXCEEDED]%s IP: %s - %s %s - Limit: %d\n", utils.ColorYellow, utils.ColorReset, clientIP, method, path, maxRequests)
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"error": "Too many requests. Please try again later.",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Log normal access with remaining limit
|
||||
fmt.Printf("[Rate Limit] IP: %s - %s %s - Used: %d/%d - Remaining: %d\n", clientIP, method, path, count, maxRequests, remaining)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// DynamicRateLimitMiddleware - Database'den ayarları okuyan rate limit middleware
|
||||
func DynamicRateLimitMiddleware(settingName string, settingsService *services.SettingsService) gin.HandlerFunc {
|
||||
cacheService := services.NewCacheService()
|
||||
|
||||
return func(c *gin.Context) {
|
||||
// Get client IP
|
||||
clientIP := c.ClientIP()
|
||||
path := c.Request.URL.Path
|
||||
method := c.Request.Method
|
||||
|
||||
// Skip checks for localhost
|
||||
if clientIP == "::1" || clientIP == "127.0.0.1" || clientIP == "localhost" {
|
||||
fmt.Printf("%s[LOCALHOST BYPASS]%s IP: %s accessed %s %s\n", utils.ColorCyan, utils.ColorReset, clientIP, method, path)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Check Blacklist from DB
|
||||
blacklist, err := settingsService.GetActiveBlacklistOrigins()
|
||||
if err == nil {
|
||||
for _, blocked := range blacklist {
|
||||
if blocked == clientIP || strings.Contains(blocked, clientIP) {
|
||||
fmt.Printf("%s[BLACKLIST BLOCKED]%s IP: %s tried to access %s %s\n", utils.ColorRed, utils.ColorReset, clientIP, method, path)
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Access denied. Your IP is blacklisted.",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check Whitelist from DB (Skip Rate Limit)
|
||||
whitelist, err := settingsService.GetActiveWhitelistOrigins()
|
||||
if err == nil {
|
||||
for _, allowed := range whitelist {
|
||||
if allowed == clientIP || strings.Contains(allowed, clientIP) {
|
||||
fmt.Printf("%s[WHITELIST ALLOWED]%s IP: %s accessed %s %s (Rate Limit Skipped)\n", utils.ColorGreen, utils.ColorReset, clientIP, method, path)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get rate limit settings from database/cache
|
||||
setting, err := settingsService.GetRateLimitSettingByName(settingName)
|
||||
if err != nil || setting == nil {
|
||||
// If error or not found, use default and allow
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if setting is active
|
||||
if !setting.IsActive {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
key := settingName + ":" + clientIP
|
||||
|
||||
// Increment counter
|
||||
duration := time.Duration(setting.WindowSeconds) * time.Second
|
||||
count, err := cacheService.IncrementRateLimit(key, duration)
|
||||
if err != nil {
|
||||
// If Redis is down, allow the request
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
remaining := setting.MaxRequests - count
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
|
||||
// Check if limit exceeded
|
||||
if count > setting.MaxRequests {
|
||||
fmt.Printf("%s[RATE LIMIT EXCEEDED]%s IP: %s - %s %s - Limit: %d\n", utils.ColorYellow, utils.ColorReset, clientIP, method, path, setting.MaxRequests)
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"error": "Too many requests. Please try again later.",
|
||||
"limit": setting.MaxRequests,
|
||||
"window": setting.WindowSeconds,
|
||||
"retry_after": setting.WindowSeconds,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Log normal access with remaining limit
|
||||
fmt.Printf("[Rate Limit] IP: %s - %s %s - Used: %d/%d - Remaining: %d\n", clientIP, method, path, count, setting.MaxRequests, remaining)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// LoginRateLimitMiddleware limits login attempts per IP
|
||||
func LoginRateLimitMiddleware() gin.HandlerFunc {
|
||||
return RateLimitMiddleware(5, 1*time.Minute) // 5 login attempts per minute
|
||||
}
|
||||
|
||||
// RegisterRateLimitMiddleware limits registration attempts per IP
|
||||
func RegisterRateLimitMiddleware() gin.HandlerFunc {
|
||||
return RateLimitMiddleware(3, 5*time.Minute) // 3 registration attempts per 5 minutes
|
||||
}
|
||||
|
||||
// APIRateLimitMiddleware general API rate limiting
|
||||
func APIRateLimitMiddleware() gin.HandlerFunc {
|
||||
return RateLimitMiddleware(100, 1*time.Minute) // 100 requests per minute
|
||||
}
|
||||
141
api/routes/routes.go
Normal file
141
api/routes/routes.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"gauth-central/api/handlers"
|
||||
"gauth-central/api/middlewares"
|
||||
_ "gauth-central/docs" // docs import
|
||||
"gauth-central/internal/services"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
func SetupRoutes(r *gin.Engine) {
|
||||
jwtService := services.NewJWTService()
|
||||
authService := services.NewAuthService()
|
||||
authHandler := handlers.NewAuthHandler(authService)
|
||||
|
||||
settingsService := services.NewSettingsService()
|
||||
settingsHandler := handlers.NewSettingsHandler(settingsService)
|
||||
|
||||
userManagementService := services.NewUserManagementService()
|
||||
userManagementHandler := handlers.NewUserManagementHandler(userManagementService)
|
||||
|
||||
avatarHandler := handlers.NewAvatarHandler()
|
||||
profileHandler := handlers.NewProfileHandler()
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
v1 := r.Group("/v1")
|
||||
v1.Use(middlewares.APIRateLimitMiddleware()) // General API rate limiting
|
||||
{
|
||||
// Swagger
|
||||
v1.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
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"),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// User endpoints
|
||||
user := v1.Group("/user")
|
||||
user.Use(middlewares.AuthMiddleware(jwtService))
|
||||
{
|
||||
// Avatar management
|
||||
user.POST("/avatar", avatarHandler.UploadAvatar)
|
||||
user.DELETE("/avatar", avatarHandler.DeleteAvatar)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user