-
+
-
+
-
diff --git a/server/internal/dto/user.go b/server/internal/dto/user.go
index a140e44..8e2703f 100644
--- a/server/internal/dto/user.go
+++ b/server/internal/dto/user.go
@@ -20,6 +20,12 @@ type CreateUserDto struct {
Role string `json:"role" binding:"required,oneof=admin user"`
}
+type UpdateUserDto struct {
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+ Role string `json:"role,omitempty" binding:"oneof=admin user"`
+}
+
func MapUser(u models.User) UserDto {
return UserDto{
ID: u.ID,
diff --git a/server/internal/handlers/users.go b/server/internal/handlers/users.go
index 7e553ec..7e86078 100644
--- a/server/internal/handlers/users.go
+++ b/server/internal/handlers/users.go
@@ -137,3 +137,82 @@ func (h *UsersHandler) GetProfile(c *gin.Context) {
}
c.JSON(http.StatusOK, dto.MapUser(u))
}
+
+// PUT /users/:id (admin) — update username, password and/or role
+func (h *UsersHandler) Update(c *gin.Context) {
+ idStr := c.Param("id")
+ id, _ := strconv.Atoi(idStr)
+ if id <= 0 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
+ return
+ }
+
+ var u models.User
+ if err := h.db.First(&u, id).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
+ return
+ }
+
+ var req dto.UpdateUserDto
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ updated := false
+
+ // --- Update username ---
+ if strings.TrimSpace(req.Username) != "" {
+ name := strings.TrimSpace(req.Username)
+ if name == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "username cannot be empty"})
+ return
+ }
+ u.Username = name
+ updated = true
+ }
+
+ // --- Update password ---
+ if req.Password != "" {
+ if len(req.Password) < 4 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "password too short"})
+ return
+ }
+ hash, err := crypto.Hash(req.Password, crypto.DefaultArgon2)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "hash error"})
+ return
+ }
+ u.Password = hash
+ updated = true
+ }
+
+ // --- Update role ---
+ if strings.TrimSpace(req.Role) != "" {
+ role := strings.ToLower(strings.TrimSpace(req.Role))
+ if role != "admin" && role != "user" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid role"})
+ return
+ }
+ u.Role = models.Role(role)
+ updated = true
+ }
+
+ if !updated {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "nothing to update"})
+ return
+ }
+
+ if err := h.db.Save(&u).Error; err != nil {
+ // detect duplicate username
+ e := strings.ToLower(err.Error())
+ if strings.Contains(e, "duplicate") || strings.Contains(e, "unique") || strings.Contains(e, "exists") {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "username already exists"})
+ return
+ }
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "update failed"})
+ return
+ }
+
+ c.JSON(http.StatusOK, dto.MapUser(u))
+}
diff --git a/server/internal/router/router.go b/server/internal/router/router.go
index 5d9bbd9..3553b1b 100644
--- a/server/internal/router/router.go
+++ b/server/internal/router/router.go
@@ -50,7 +50,7 @@ func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine {
r.POST("/auth/change_password", authMW, authH.ChangePassword)
r.GET("/users/profile", authMW, usersH.Profile)
- r.POST("/users/:id/set_role", authMW, adminOnly, usersH.SetRole)
+ r.PUT("/users/:id", authMW, adminOnly, usersH.Update)
r.GET("/users", authMW, adminOnly, usersH.List)
r.POST("/users/create", authMW, adminOnly, usersH.Create)
r.DELETE("/users/:id", authMW, adminOnly, usersH.Delete)