enhance bug report handling with pagination, filtering, and improved response structure
This commit is contained in:
263
internal/handlers/admin_users.route.go
Normal file
263
internal/handlers/admin_users.route.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"emly-api-go/internal/models"
|
||||
)
|
||||
|
||||
// ListUsers handles GET /v1/api/admin/users
|
||||
func ListUsers(db *sqlx.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var users []models.User
|
||||
if err := db.SelectContext(r.Context(), &users,
|
||||
"SELECT id, username, displayname, role, enabled, created_at FROM `user` ORDER BY created_at ASC",
|
||||
); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, users)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser handles POST /v1/api/admin/users
|
||||
func CreateUser(db *sqlx.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Displayname string `json:"displayname"`
|
||||
Password string `json:"password"`
|
||||
Role models.UserRole `json:"role"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
if body.Username == "" || body.Password == "" {
|
||||
jsonError(w, http.StatusBadRequest, "username and password are required")
|
||||
return
|
||||
}
|
||||
if body.Role != models.UserRoleAdmin && body.Role != models.UserRoleUser {
|
||||
jsonError(w, http.StatusBadRequest, "role must be 'admin' or 'user'")
|
||||
return
|
||||
}
|
||||
|
||||
var count int
|
||||
if err := db.GetContext(r.Context(), &count,
|
||||
"SELECT COUNT(*) FROM `user` WHERE username = ?", body.Username,
|
||||
); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
jsonError(w, http.StatusConflict, "username already exists")
|
||||
return
|
||||
}
|
||||
|
||||
passwordHash, err := hashPassword(body.Password)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, "failed to hash password: "+err.Error())
|
||||
return
|
||||
}
|
||||
id, err := generateUUID()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, "failed to generate id: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(r.Context(),
|
||||
"INSERT INTO `user` (id, username, displayname, password_hash, role) VALUES (?, ?, ?, ?, ?)",
|
||||
id, body.Username, body.Displayname, passwordHash, body.Role,
|
||||
); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.GetContext(r.Context(), &user,
|
||||
"SELECT id, username, displayname, role, enabled, created_at FROM `user` WHERE id = ?", id,
|
||||
); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonCreated(w, user)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserByID handles GET /v1/api/admin/users/{id}
|
||||
func GetUserByID(db *sqlx.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
jsonError(w, http.StatusBadRequest, "missing id parameter")
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err := db.GetContext(r.Context(), &user,
|
||||
"SELECT id, username, displayname, role, enabled, created_at FROM `user` WHERE id = ? LIMIT 1", id,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
jsonError(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, user)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateUser handles PATCH /v1/api/admin/users/{id}
|
||||
// Accepted fields: displayname, enabled
|
||||
func UpdateUser(db *sqlx.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
jsonError(w, http.StatusBadRequest, "missing id parameter")
|
||||
return
|
||||
}
|
||||
|
||||
var body map[string]json.RawMessage
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fields := []string{}
|
||||
params := []any{}
|
||||
|
||||
if raw, ok := body["displayname"]; ok {
|
||||
var v string
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid displayname value")
|
||||
return
|
||||
}
|
||||
fields = append(fields, "displayname = ?")
|
||||
params = append(params, v)
|
||||
}
|
||||
if raw, ok := body["enabled"]; ok {
|
||||
var v bool
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid enabled value")
|
||||
return
|
||||
}
|
||||
fields = append(fields, "enabled = ?")
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
jsonError(w, http.StatusBadRequest, "no updatable fields provided")
|
||||
return
|
||||
}
|
||||
|
||||
query := "UPDATE `user` SET "
|
||||
for i, f := range fields {
|
||||
if i > 0 {
|
||||
query += ", "
|
||||
}
|
||||
query += f
|
||||
}
|
||||
query += " WHERE id = ?"
|
||||
params = append(params, id)
|
||||
|
||||
result, err := db.ExecContext(r.Context(), query, params...)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if rows == 0 {
|
||||
jsonError(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
jsonOK(w, map[string]bool{"updated": true})
|
||||
}
|
||||
}
|
||||
|
||||
// ResetPassword handles POST /v1/api/admin/users/{id}/reset-password
|
||||
func ResetPassword(db *sqlx.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
jsonError(w, http.StatusBadRequest, "missing id parameter")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
if body.Password == "" {
|
||||
jsonError(w, http.StatusBadRequest, "password is required")
|
||||
return
|
||||
}
|
||||
|
||||
passwordHash, err := hashPassword(body.Password)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, "failed to hash password: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := db.ExecContext(r.Context(),
|
||||
"UPDATE `user` SET password_hash = ? WHERE id = ?", passwordHash, id,
|
||||
)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if rows == 0 {
|
||||
jsonError(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
jsonOK(w, map[string]bool{"updated": true})
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUser handles DELETE /v1/api/admin/users/{id}
|
||||
func DeleteUser(db *sqlx.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
jsonError(w, http.StatusBadRequest, "missing id parameter")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := db.ExecContext(r.Context(),
|
||||
"DELETE FROM `user` WHERE id = ?", id,
|
||||
)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if rows == 0 {
|
||||
jsonError(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
jsonOK(w, map[string]bool{"deleted": true})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user