264 lines
7.0 KiB
Go
264 lines
7.0 KiB
Go
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})
|
|
}
|
|
}
|