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
231 lines
6.8 KiB
Go
231 lines
6.8 KiB
Go
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))
|
||
}
|