Files
user-system/internal/api/handler/avatar_handler.go
long-agent 0564bfd9ad docs: add Swagger annotations to 13 API handlers
Added @Summary, @Description, @Tags, @Param, @Success, @Failure,
@Router annotations to all major handler endpoints for OpenAPI/Swagger
auto-generation. Covers 86 annotations across:

- auth_handler.go (25): all auth endpoints
- user_handler.go (14): CRUD + roles + admin management
- device_handler.go (13): device CRUD + trust management
- role_handler.go (8): role CRUD + permissions
- custom_field_handler.go (7): field CRUD + user values
- permission_handler.go (7): permission CRUD + tree
- log_handler.go (3): login/operation logs
- captcha_handler.go (3): generate/verify
- stats_handler.go (2): dashboard + user stats
- avatar_handler.go (1): upload avatar
- totp_handler.go (1): totp status
- password_reset_handler.go (1): forgot password

Partially addresses P2: missing Swagger annotations
(PRODUCTION_GAP_ANALYSIS_2026-04-08)
2026-04-11 21:23:52 +08:00

161 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/domain"
"github.com/user-management-system/internal/repository"
)
// AvatarHandler handles avatar upload requests
type AvatarHandler struct {
userRepo *repository.UserRepository
}
// NewAvatarHandler creates a new AvatarHandler
func NewAvatarHandler(userRepo *repository.UserRepository) *AvatarHandler {
return &AvatarHandler{userRepo: userRepo}
}
// generateSecureToken generates a secure random token
func generateSecureToken(length int) string {
bytes := make([]byte, length)
rand.Read(bytes)
return hex.EncodeToString(bytes)[:length]
}
// UploadAvatar 上传用户头像
// @Summary 上传用户头像
// @Description 上传并更新用户头像(仅本人或管理员)
// @Tags 用户头像
// @Accept multipart/form-data
// @Produce json
// @Security BearerAuth
// @Param id path int true "用户ID"
// @Param avatar formData file true "头像文件最大5MB支持jpg/jpeg/png/gif/webp"
// @Success 200 {object} Response{data=AvatarResponse} "上传成功"
// @Failure 400 {object} Response "文件无效或大小超限"
// @Failure 401 {object} Response "未认证"
// @Failure 403 {object} Response "无权限"
// @Failure 404 {object} Response "用户不存在"
// @Router /api/v1/users/{id}/avatar [post]
func (h *AvatarHandler) UploadAvatar(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
return
}
// Get current user from context (set by auth middleware)
currentUserID := c.GetInt64("user_id")
if currentUserID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"})
return
}
// Check permission: user can only update their own avatar, or admin can update any
isAdmin := false
if roles, ok := c.Get("user_roles"); ok {
for _, role := range roles.([]*domain.Role) {
if role.Code == "admin" {
isAdmin = true
break
}
}
}
if currentUserID != userID && !isAdmin {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "permission denied"})
return
}
// Get file from form
file, err := c.FormFile("avatar")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "no avatar file provided"})
return
}
// Validate file size (max 5MB)
if file.Size > 5*1024*1024 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "file size exceeds 5MB limit"})
return
}
// Validate file type
ext := filepath.Ext(file.Filename)
allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true}
if !allowedExts[ext] {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid file type, allowed: jpg, jpeg, png, gif, webp"})
return
}
// Open the uploaded file
src, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to open uploaded file"})
return
}
defer src.Close()
// Generate unique filename
avatarFilename := fmt.Sprintf("avatar_%d_%s%s", userID, generateSecureToken(8), ext)
uploadDir := "./uploads/avatars"
// Create upload directory if not exists
if err := os.MkdirAll(uploadDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to create upload directory"})
return
}
// Save file to disk
dstPath := filepath.Join(uploadDir, avatarFilename)
data := make([]byte, file.Size)
if _, err := src.Read(data); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to read uploaded file"})
return
}
if err := os.WriteFile(dstPath, data, 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to save avatar file"})
return
}
// Generate avatar URL (in production, this would be a CDN URL)
avatarURL := fmt.Sprintf("/uploads/avatars/%s", avatarFilename)
// Update user's avatar in database
user, err := h.userRepo.GetByID(c.Request.Context(), userID)
if err != nil {
// Clean up the uploaded file
os.Remove(dstPath)
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "user not found"})
return
}
user.Avatar = avatarURL
if err := h.userRepo.Update(c.Request.Context(), user); err != nil {
// Clean up the uploaded file
os.Remove(dstPath)
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to update user avatar"})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "avatar uploaded successfully",
"data": gin.H{
"avatar_url": avatarURL,
"thumbnail": avatarURL,
},
})
}