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
175 lines
4.7 KiB
Go
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)
|
|
}
|