first commit

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

View File

@@ -0,0 +1,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,
})
}