enhance bug report handling with pagination, filtering, and improved response structure

This commit is contained in:
Flavio Fois
2026-03-19 09:00:07 +01:00
parent 9df575067a
commit da650c2b82
13 changed files with 708 additions and 16 deletions

View 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})
}
}