hako/internal/server/handlers/users.go
Felipe M. 484b225af7
feat: add user administration API and UI
Add admin-only CRUD endpoints for user management under /api/v1/system/users
with full frontend implementation including create, edit, change password,
and delete operations with self-protection guards.

- Extend AuthDomain interface with ListUsers, GetUser, UpdateUser,
  UpdateUserPassword, DeleteUser methods
- Add List, Update, Delete methods to UserStore with shared scanUser helper
- Create UserHandler with 5 endpoints protected by admin middleware
- Complete Users.vue admin page with modals and error handling
- Make CreateUser accept role parameter for atomic user creation
- Fix auth middleware to use database role as source of truth instead of
  stale JWT claims, preventing privilege persistence after demotion
- Fix pre-existing gofmt issues in yt_dlp.go and config.go
2026-04-02 10:33:36 +02:00

175 lines
4.7 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"time"
"git.nakama.town/fmartingr/hako/internal/model"
"git.nakama.town/fmartingr/hako/internal/server/webcontext"
)
// UserHandler handles user administration requests
type UserHandler struct {
deps model.Dependencies
}
// NewUserHandler creates a new UserHandler
func NewUserHandler(deps model.Dependencies) *UserHandler {
return &UserHandler{deps: deps}
}
// AdminUserResponse represents a user in admin API responses
type AdminUserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func userToAdminResponse(u *model.User) AdminUserResponse {
return AdminUserResponse{
ID: u.ID.String(),
Email: u.Email,
Role: u.Role,
CreatedAt: u.CreatedAt.Format(time.RFC3339),
UpdatedAt: u.UpdatedAt.Format(time.RFC3339),
}
}
// writeJSON writes a JSON response with the given status code
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(data)
}
// HandleListUsers handles GET /system/users
func (h *UserHandler) HandleListUsers(w http.ResponseWriter, r *http.Request) {
users, err := h.deps.Domains().Auth().ListUsers()
if err != nil {
http.Error(w, "Failed to list users", http.StatusInternalServerError)
return
}
response := make([]AdminUserResponse, len(users))
for i, u := range users {
response[i] = userToAdminResponse(u)
}
writeJSON(w, http.StatusOK, response)
}
// CreateUserRequest represents a request to create a user
type CreateUserRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Role string `json:"role"`
}
// HandleCreateUser handles POST /system/users
func (h *UserHandler) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Email == "" || req.Password == "" {
http.Error(w, "Email and password are required", http.StatusBadRequest)
return
}
user, err := h.deps.Domains().Auth().CreateUser(req.Email, req.Password, req.Role)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeJSON(w, http.StatusCreated, userToAdminResponse(user))
}
// UpdateUserRequest represents a request to update a user
type UpdateUserRequest struct {
Email string `json:"email"`
Role string `json:"role"`
}
// HandleUpdateUser handles PUT /system/users/{id}
func (h *UserHandler) HandleUpdateUser(w http.ResponseWriter, r *http.Request) {
c := webcontext.NewWebContext(w, r)
id := r.PathValue("id")
var req UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Email == "" || req.Role == "" {
http.Error(w, "Email and role are required", http.StatusBadRequest)
return
}
// Self-protection: prevent admin from removing their own admin role
if id == c.GetUserID() && req.Role != "admin" {
http.Error(w, "Cannot remove your own admin role", http.StatusBadRequest)
return
}
user, err := h.deps.Domains().Auth().UpdateUser(id, req.Email, req.Role)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeJSON(w, http.StatusOK, userToAdminResponse(user))
}
// UpdatePasswordRequest represents a request to change a user's password
type UpdatePasswordRequest struct {
Password string `json:"password"`
}
// HandleUpdatePassword handles PUT /system/users/{id}/password
func (h *UserHandler) HandleUpdatePassword(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var req UpdatePasswordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Password == "" {
http.Error(w, "Password is required", http.StatusBadRequest)
return
}
if err := h.deps.Domains().Auth().UpdateUserPassword(id, req.Password); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}
// HandleDeleteUser handles DELETE /system/users/{id}
func (h *UserHandler) HandleDeleteUser(w http.ResponseWriter, r *http.Request) {
c := webcontext.NewWebContext(w, r)
id := r.PathValue("id")
// Self-protection: prevent admin from deleting themselves
if id == c.GetUserID() {
http.Error(w, "Cannot delete your own account", http.StatusBadRequest)
return
}
if err := h.deps.Domains().Auth().DeleteUser(id); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}