327 lines
9.0 KiB
Go
327 lines
9.0 KiB
Go
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,
|
|
})
|
|
}
|