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)
161 lines
4.7 KiB
Go
161 lines
4.7 KiB
Go
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,
|
||
},
|
||
})
|
||
}
|