Maksim Totmin 40b8afb150 refactor: common util package, ESL/AMI/security fixes, Prometheus metrics
Backend stability and security improvements:

* internal/util/ — common RandomHex helper, removed 3 duplicates
* ESL: deduplicated readMessage (locked/unlocked), net.JoinHostPort for IPv6
* AMI: synchronous reconnect() in readEventsLoop, net.JoinHostPort for IPv6
* Auth: /api/auth/refresh accepts Authorization header only (no ?token=)
* decodeJSON: http.MaxBytesReader(1<<20) body limit
* Trunks: gatewayParams() uses configured ESL.GatewayPrefix
* Config: jwt_secret_env env-var fallback
* FSCollector: time.After → time.NewTimer with defer Stop
* Monitoring: Prometheus counters (route_requests, nodes_total/healthy, uptime)
* go fmt pass across all internal/ packages
2026-06-25 19:30:36 +07:00

231 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"fmt"
"net/http"
"time"
"github.com/pulse-lets-go/internal/models"
"github.com/pulse-lets-go/internal/util"
"golang.org/x/crypto/bcrypt"
)
// handleGetUsers — GET /api/users — список пользователей.
func (a *API) handleGetUsers(w http.ResponseWriter, r *http.Request) {
users, err := a.configManager.ReadUsers()
if err != nil {
writeError(w, http.StatusInternalServerError, "ошибка чтения пользователей")
return
}
// Не возвращаем хэши паролей
type safeUser struct {
ID string `json:"id"`
Username string `json:"username"`
Role models.UserRole `json:"role"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
result := make([]safeUser, len(users))
for i, u := range users {
result[i] = safeUser{
ID: u.ID,
Username: u.Username,
Role: u.Role,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
writeJSON(w, http.StatusOK, result)
}
// handleCreateUser — POST /api/users — создание пользователя.
func (a *API) handleCreateUser(w http.ResponseWriter, r *http.Request) {
var req models.CreateUserRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "некорректный JSON")
return
}
if req.Username == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "имя и пароль обязательны")
return
}
if req.Role != models.RoleAdmin && req.Role != models.RoleViewer {
writeError(w, http.StatusBadRequest, "роль должна быть admin или viewer")
return
}
users, err := a.configManager.ReadUsers()
if err != nil {
writeError(w, http.StatusInternalServerError, "ошибка чтения пользователей")
return
}
// Проверка уникальности имени
for _, u := range users {
if u.Username == req.Username {
writeError(w, http.StatusConflict, "пользователь с таким именем уже существует")
return
}
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
writeError(w, http.StatusInternalServerError, "ошибка хеширования пароля")
return
}
now := time.Now().UTC()
user := models.User{
ID: generateID("user"),
Username: req.Username,
PasswordHash: string(hash),
Role: req.Role,
CreatedAt: now,
UpdatedAt: now,
}
users = append(users, user)
if err := a.configManager.SaveUsers(users); err != nil {
writeError(w, http.StatusInternalServerError, "ошибка сохранения пользователя")
return
}
writeJSON(w, http.StatusCreated, map[string]string{
"id": user.ID,
"username": user.Username,
"role": string(user.Role),
})
}
// handleUpdateUser — PUT /api/users/{id} — обновление пользователя.
func (a *API) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
if userID == "" {
writeError(w, http.StatusBadRequest, "не указан id пользователя")
return
}
var req models.UpdateUserRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "некорректный JSON")
return
}
users, err := a.configManager.ReadUsers()
if err != nil {
writeError(w, http.StatusInternalServerError, "ошибка чтения пользователей")
return
}
idx := findUserIndex(users, userID)
if idx < 0 {
writeError(w, http.StatusNotFound, "пользователь не найден")
return
}
u := &users[idx]
if req.Username != nil {
// Проверка уникальности
for i, other := range users {
if other.Username == *req.Username && i != idx {
writeError(w, http.StatusConflict, "пользователь с таким именем уже существует")
return
}
}
u.Username = *req.Username
}
if req.Password != nil {
hash, _ := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
u.PasswordHash = string(hash)
}
if req.Role != nil {
if *req.Role != models.RoleAdmin && *req.Role != models.RoleViewer {
writeError(w, http.StatusBadRequest, "роль должна быть admin или viewer")
return
}
// Защита первичного администратора от понижения роли
if userID == "user-01" && *req.Role != models.RoleAdmin {
writeError(w, http.StatusConflict, "роль первичного администратора нельзя изменить")
return
}
u.Role = *req.Role
}
u.UpdatedAt = time.Now().UTC()
if err := a.configManager.SaveUsers(users); err != nil {
writeError(w, http.StatusInternalServerError, "ошибка сохранения пользователя")
return
}
writeJSON(w, http.StatusOK, map[string]string{
"id": u.ID,
"username": u.Username,
"role": string(u.Role),
})
}
// handleDeleteUser — DELETE /api/users/{id} — удаление пользователя.
func (a *API) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
if userID == "" {
writeError(w, http.StatusBadRequest, "не указан id пользователя")
return
}
users, err := a.configManager.ReadUsers()
if err != nil {
writeError(w, http.StatusInternalServerError, "ошибка чтения пользователей")
return
}
idx := findUserIndex(users, userID)
if idx < 0 {
writeError(w, http.StatusNotFound, "пользователь не найден")
return
}
// Защита первичного администратора от удаления
if userID == "user-01" {
writeError(w, http.StatusConflict, "первичного администратора нельзя удалить")
return
}
// Не даём удалить последнего админа
adminCount := 0
for _, u := range users {
if u.Role == models.RoleAdmin {
adminCount++
}
}
if users[idx].Role == models.RoleAdmin && adminCount <= 1 {
writeError(w, http.StatusConflict, "нельзя удалить последнего администратора")
return
}
users = append(users[:idx], users[idx+1:]...)
if err := a.configManager.SaveUsers(users); err != nil {
writeError(w, http.StatusInternalServerError, "ошибка сохранения пользователей")
return
}
writeJSON(w, http.StatusNoContent, nil)
}
// findUserIndex находит индекс пользователя по ID.
func findUserIndex(users []models.User, id string) int {
for i := range users {
if users[i].ID == id {
return i
}
}
return -1
}
// generateID генерирует ID вида "prefix-XXXXX".
func generateID(prefix string) string {
return fmt.Sprintf("%s-%s", prefix, util.RandomHex(6))
}