first commit
This commit is contained in:
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user