package handlers import ( "bytes" "fmt" "image" "image/jpeg" "io" "net/http" "os" "path/filepath" "strings" "time" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "gorm.io/gorm" xdraw "golang.org/x/image/draw" _ "golang.org/x/image/webp" "go_blog/models" ) // ProfilePage renders the profile edit page. func ProfilePage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) session := sessions.Default(c) userID := session.Get("user_id") var user models.User if err := db.First(&user, userID).Error; err != nil { c.String(http.StatusInternalServerError, "User not found") return } data := DefaultData(c) data["Title"] = tr["profile_title"] data["Profile"] = user // Flash messages (success / error). if msg := c.Query("saved"); msg == "1" { data["Success"] = tr["profile_saved"] } if msg := c.Query("error"); msg == "pw" { data["Error"] = tr["profile_wrong_password"] } switch c.Query("error") { case "upload": data["Error"] = tr["profile_upload_invalid"] case "upload_disabled": data["Error"] = tr["profile_upload_disabled"] case "size": data["Error"] = fmt.Sprintf(tr["profile_upload_too_large"], c.Query("max")) } c.HTML(http.StatusOK, "profile", data) } } // UpdateProfile processes the profile edit form (multipart). func UpdateProfile(db *gorm.DB, storagePath string) gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) userID := session.Get("user_id") var user models.User if err := db.First(&user, userID).Error; err != nil { c.Redirect(http.StatusFound, "/profile") return } // --- Text fields --- if v := c.PostForm("display_name"); v != "" { user.DisplayName = v } if v := c.PostForm("gender"); v != "" { user.Gender = v } if v := c.PostForm("email"); v != "" { user.Email = v } if v := c.PostForm("birthday"); v != "" { if t, err := time.Parse("2006-01-02", v); err == nil { user.Birthday = &t } } // --- Avatar upload --- file, header, err := c.Request.FormFile("avatar") if err == nil { defer file.Close() // Validate against the platform upload policy (switch + type + size). check := ValidateUpload(header) if !check.OK { session.Save() reason := "?error=upload" if !models.GetUploadConfig().Enabled { reason = "?error=upload_disabled" } else if check.Type != nil { reason = fmt.Sprintf("?error=size&max=%s", formatSize(check.MaxSize)) } c.Redirect(http.StatusFound, "/profile"+reason) return } // Determine file extension (validated to be in the whitelist). ext := strings.ToLower(filepath.Ext(header.Filename)) // Save under storagePath/avatars/. avatarDir := filepath.Join(storagePath, "avatars") if err := os.MkdirAll(avatarDir, 0755); err != nil { c.Redirect(http.StatusFound, "/profile") return } // Remove old avatar file if it exists (different extension or same). if user.Avatar != "" { oldPath := filepath.Join(avatarDir, user.Avatar) os.Remove(oldPath) // ignore error — file may not exist } // Use user ID as filename base. savedName := fmt.Sprintf("%d%s", user.ID, ext) savedPath := filepath.Join(avatarDir, savedName) dst, err := os.Create(savedPath) if err != nil { c.Redirect(http.StatusFound, "/profile") return } defer dst.Close() if _, err := io.Copy(dst, file); err != nil { c.Redirect(http.StatusFound, "/profile") return } user.Avatar = savedName session.Set("avatar", savedName) } // --- Password change --- currentPass := c.PostForm("current_password") newPass := c.PostForm("new_password") if currentPass != "" && newPass != "" { if !user.CheckPassword(currentPass) { session.Save() c.Redirect(http.StatusFound, "/profile?error=pw") return } if err := user.SetPassword(newPass); err != nil { session.Save() c.Redirect(http.StatusFound, "/profile") return } } // Save user record. if err := db.Save(&user).Error; err != nil { session.Save() c.Redirect(http.StatusFound, "/profile") return } // Update display name in session. session.Set("display_name", user.DisplayName) session.Save() c.Redirect(http.StatusFound, "/profile?saved=1") } } // UploadAvatar handles AJAX avatar upload with cropping. func UploadAvatar(db *gorm.DB, storagePath string) gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) userID := session.Get("user_id") var user models.User if err := db.First(&user, userID).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "User not found"}) return } file, header, err := c.Request.FormFile("avatar") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"}) return } defer file.Close() // Validate against the platform upload policy (switch + type + size). check := ValidateUpload(header) if !check.OK { if !models.GetUploadConfig().Enabled { c.JSON(http.StatusBadRequest, gin.H{"error": "uploads are disabled"}) return } if check.Type != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("file too large; limit is %s", formatSize(check.MaxSize))}) return } c.JSON(http.StatusBadRequest, gin.H{"error": "file type not allowed"}) return } // Determine file extension (validated to be in the whitelist). ext := strings.ToLower(filepath.Ext(header.Filename)) // Read file bytes for image processing. imgBytes, err := io.ReadAll(file) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read file"}) return } // Decode, resize, and re-encode the image. processedBytes, finalExt, err := processAvatar(imgBytes, ext) if err != nil { // Fall back to saving raw bytes if processing fails. processedBytes = imgBytes finalExt = ext } // Ensure avatar directory exists. avatarDir := filepath.Join(storagePath, "avatars") os.MkdirAll(avatarDir, 0755) // Remove old avatar file. if user.Avatar != "" { oldPath := filepath.Join(avatarDir, user.Avatar) os.Remove(oldPath) } // Save processed avatar. savedName := fmt.Sprintf("%d%s", user.ID, finalExt) savedPath := filepath.Join(avatarDir, savedName) if err := os.WriteFile(savedPath, processedBytes, 0644); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar"}) return } // Update user record. user.Avatar = savedName db.Save(&user) // Update session. session.Set("avatar", savedName) session.Save() c.JSON(http.StatusOK, gin.H{"avatar": savedName}) } } // processAvatar decodes, resizes to 256x256, and re-encodes an avatar image as JPEG. func processAvatar(imgBytes []byte, ext string) ([]byte, string, error) { src, _, err := image.Decode(bytes.NewReader(imgBytes)) if err != nil { return nil, "", err } const targetSize = 256 dst := image.NewRGBA(image.Rect(0, 0, targetSize, targetSize)) xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), xdraw.Src, nil) var buf bytes.Buffer if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: 85}); err != nil { return nil, "", err } return buf.Bytes(), ".jpg", nil }