diff --git a/server/internal/api/admin.go b/server/internal/api/admin.go index aaae553..6d1759d 100644 --- a/server/internal/api/admin.go +++ b/server/internal/api/admin.go @@ -3,7 +3,9 @@ package api import ( "encoding/json" "net/http" + "net/mail" "strconv" + "strings" "time" "github.com/gin-gonic/gin" @@ -46,14 +48,57 @@ func (h *Handler) AdminPatchUser(c *gin.Context) { return } var req struct { - Role *string `json:"role"` - Status *string `json:"status"` + Username *string `json:"username"` + Email *string `json:"email"` + Password *string `json:"password"` + Role *string `json:"role"` + Status *string `json:"status"` } if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input") return } updates := map[string]any{} + if req.Username != nil { + u := strings.TrimSpace(*req.Username) + if len(u) < 3 || len(u) > 32 { + resp.Fail(c, http.StatusBadRequest, "username must be 3-32 chars") + return + } + var n int64 + h.a.DB.Model(&store.User{}).Where("username = ? AND id != ?", u, id).Count(&n) + if n > 0 { + resp.Fail(c, http.StatusConflict, "username already taken") + return + } + updates["username"] = u + } + if req.Email != nil { + e := strings.ToLower(strings.TrimSpace(*req.Email)) + if _, err := mail.ParseAddress(e); err != nil { + resp.Fail(c, http.StatusBadRequest, "invalid email") + return + } + var n int64 + h.a.DB.Model(&store.User{}).Where("email = ? AND id != ?", e, id).Count(&n) + if n > 0 { + resp.Fail(c, http.StatusConflict, "email already taken") + return + } + updates["email"] = e + } + if req.Password != nil && *req.Password != "" { + if len(*req.Password) < 8 { + resp.Fail(c, http.StatusBadRequest, "password must be at least 8 chars") + return + } + hash, err := h.a.Hasher.HashPassword(*req.Password) + if err != nil { + resp.Fail(c, http.StatusInternalServerError, "failed to hash password") + return + } + updates["password_hash"] = hash + } if req.Role != nil { if *req.Role != store.RoleUser && *req.Role != store.RoleAdmin { resp.Fail(c, http.StatusBadRequest, "role must be user or admin") diff --git a/web/src/views/admin/UsersView.vue b/web/src/views/admin/UsersView.vue index 88441e9..aea7738 100644 --- a/web/src/views/admin/UsersView.vue +++ b/web/src/views/admin/UsersView.vue @@ -20,7 +20,7 @@ const pageSize = 15 const editOpen = ref(false) const editing = ref(null) -const editForm = reactive({ role: 'user', status: 'active' }) +const editForm = reactive({ username: '', email: '', password: '', role: 'user', status: 'active' }) const balanceOpen = ref(false) const balanceUser = ref(null) @@ -43,7 +43,13 @@ function search() { function openEdit(u: User) { editing.value = u - Object.assign(editForm, { role: u.role, status: u.status }) + Object.assign(editForm, { + username: u.username, + email: u.email, + password: '', + role: u.role, + status: u.status, + }) editOpen.value = true } @@ -160,22 +166,33 @@ onMounted(load) - +
- - + + + +
+ + +