Files
go_blog/handlers/profile.go
T

245 lines
6.1 KiB
Go

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"]
}
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()
// Determine file extension.
ext := strings.ToLower(filepath.Ext(header.Filename))
if ext == "" || (ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" && ext != ".webp") {
ext = ".jpg"
}
// 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 extension.
ext := strings.ToLower(filepath.Ext(header.Filename))
if ext == "" || (ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" && ext != ".webp") {
ext = ".jpg"
}
// 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
}