Files
rmser/internal/transport/http/handlers/settings.go
SERTY 88620f3fb6 0202-финиш перед десктопом
пересчет поправил
редактирование с перепроведением
галка автопроведения работает
рекомендации починил
2026-02-02 13:53:38 +03:00

543 lines
16 KiB
Go
Raw Permalink 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 handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"rmser/internal/domain/account"
"rmser/internal/domain/catalog"
"rmser/pkg/logger"
)
// Notifier - интерфейс для отправки уведомлений (реализуется ботом)
type Notifier interface {
SendRoleChangeNotification(telegramID int64, serverName string, newRole string)
SendRemovalNotification(telegramID int64, serverName string)
}
type SettingsHandler struct {
accountRepo account.Repository
catalogRepo catalog.Repository
notifier Notifier // Поле для отправки уведомлений
rmsFactory RMSFactory
}
type RMSFactory interface {
ClearCacheForUser(userID uuid.UUID)
}
func NewSettingsHandler(accRepo account.Repository, catRepo catalog.Repository) *SettingsHandler {
return &SettingsHandler{
accountRepo: accRepo,
catalogRepo: catRepo,
}
}
// SetRMSFactory используется для внедрения зависимости после инициализации
func (h *SettingsHandler) SetRMSFactory(f RMSFactory) {
h.rmsFactory = f
}
// SetNotifier используется для внедрения зависимости после инициализации
func (h *SettingsHandler) SetNotifier(n Notifier) {
h.notifier = n
}
// SettingsResponse - DTO для отдачи настроек
type SettingsResponse struct {
ID string `json:"id"`
Name string `json:"name"`
BaseURL string `json:"base_url"`
DefaultStoreID *string `json:"default_store_id"` // Nullable
RootGroupID *string `json:"root_group_id"` // Nullable
AutoConduct bool `json:"auto_conduct"`
Role string `json:"role"` // OWNER, ADMIN, OPERATOR
SyncInterval int `json:"sync_interval"` // Интервал синхронизации в минутах
LastSyncAt *time.Time `json:"last_sync_at"` // Время последней синхронизации
LastActivityAt *time.Time `json:"last_activity_at"` // Время последней активности
}
// GetSettings возвращает настройки активного сервера + роль пользователя
func (h *SettingsHandler) GetSettings(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
return
}
role, err := h.accountRepo.GetUserRole(userID, server.ID)
if err != nil {
role = account.RoleOperator
}
resp := SettingsResponse{
ID: server.ID.String(),
Name: server.Name,
BaseURL: server.BaseURL,
AutoConduct: server.AutoProcess,
Role: string(role),
SyncInterval: server.SyncInterval,
LastSyncAt: server.LastSyncAt,
LastActivityAt: server.LastActivityAt,
}
if server.DefaultStoreID != nil {
s := server.DefaultStoreID.String()
resp.DefaultStoreID = &s
}
if server.RootGroupGUID != nil {
s := server.RootGroupGUID.String()
resp.RootGroupID = &s
}
c.JSON(http.StatusOK, resp)
}
// UpdateSettingsDTO - DTO для частичного обновления настроек (PATCH-семантика)
type UpdateSettingsDTO struct {
Name *string `json:"name"`
DefaultStoreID *string `json:"default_store_id"`
RootGroupID *string `json:"root_group_id"`
AutoProcess *bool `json:"auto_process"` // Legacy для обратной совместимости
AutoConduct *bool `json:"auto_conduct"` // Новое поле
SyncInterval *int `json:"sync_interval,omitempty"` // Интервал синхронизации в минутах (5 - 10080)
}
// UpdateSettings сохраняет настройки с PATCH-семантикой
func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
var req UpdateSettingsDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Логирование полученных данных для отладки
logger.Log.Info("Получен запрос на обновление настроек",
zap.Any("request", req),
)
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
return
}
// ПРОВЕРКА ПРАВ
role, err := h.accountRepo.GetUserRole(userID, server.ID)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": "Access check failed"})
return
}
if role != account.RoleOwner && role != account.RoleAdmin {
c.JSON(http.StatusForbidden, gin.H{"error": "У вас нет прав на изменение настроек сервера (требуется ADMIN или OWNER)"})
return
}
// Обновление имени (только если передано)
if req.Name != nil {
server.Name = *req.Name
}
// Обновление флага авто-проведения
if req.AutoConduct != nil {
server.AutoProcess = *req.AutoConduct
} else if req.AutoProcess != nil {
// Fallback для старых клиентов, которые используют legacy поле
server.AutoProcess = *req.AutoProcess
}
// Обновление интервала синхронизации
if req.SyncInterval != nil {
// Валидация диапазона: от 5 минут до 1 недели (10080 минут)
if *req.SyncInterval < 5 || *req.SyncInterval > 10080 {
c.JSON(http.StatusBadRequest, gin.H{"error": "sync_interval должен быть от 5 минут до 1 недели (10080 минут)"})
return
}
server.SyncInterval = *req.SyncInterval
}
// Обновление DefaultStoreID
if req.DefaultStoreID != nil {
if *req.DefaultStoreID == "" {
// Пустая строка -> сбрасываем в nil
server.DefaultStoreID = nil
} else {
// UUID -> обновляем
if uid, err := uuid.Parse(*req.DefaultStoreID); err == nil {
server.DefaultStoreID = &uid
}
}
}
// Если nil -> не трогаем текущее значение
// Обновление RootGroupID
if req.RootGroupID != nil {
if *req.RootGroupID == "" {
// Пустая строка -> сбрасываем в nil
server.RootGroupGUID = nil
} else {
// UUID -> обновляем
if uid, err := uuid.Parse(*req.RootGroupID); err == nil {
server.RootGroupGUID = &uid
}
}
}
// Если nil -> не трогаем текущее значение
if err := h.accountRepo.SaveServerSettings(server); err != nil {
logger.Log.Error("Failed to save settings", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.GetSettings(c)
}
// --- Group Tree Logic ---
type GroupNode struct {
Key string `json:"key"`
Value string `json:"value"`
Title string `json:"title"`
Children []*GroupNode `json:"children"`
}
func (h *SettingsHandler) GetGroupsTree(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
return
}
groups, err := h.catalogRepo.GetGroups(server.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
tree := buildTree(groups)
c.JSON(http.StatusOK, tree)
}
func buildTree(flat []catalog.Product) []*GroupNode {
nodeMap := make(map[uuid.UUID]*GroupNode)
for _, g := range flat {
nodeMap[g.ID] = &GroupNode{
Key: g.ID.String(),
Value: g.ID.String(),
Title: g.Name,
Children: make([]*GroupNode, 0),
}
}
var roots []*GroupNode
for _, g := range flat {
node := nodeMap[g.ID]
if g.ParentID != nil {
if parent, exists := nodeMap[*g.ParentID]; exists {
parent.Children = append(parent.Children, node)
} else {
roots = append(roots, node)
}
} else {
roots = append(roots, node)
}
}
return roots
}
// --- User Management ---
func (h *SettingsHandler) GetServerUsers(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
return
}
myRole, err := h.accountRepo.GetUserRole(userID, server.ID)
if err != nil || (myRole != account.RoleOwner && myRole != account.RoleAdmin) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
users, err := h.accountRepo.GetServerUsers(server.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
type UserDTO struct {
UserID uuid.UUID `json:"user_id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhotoURL string `json:"photo_url"`
Role account.Role `json:"role"`
IsMe bool `json:"is_me"`
}
response := make([]UserDTO, 0, len(users))
for _, u := range users {
response = append(response, UserDTO{
UserID: u.UserID,
Username: u.User.Username,
FirstName: u.User.FirstName,
LastName: u.User.LastName,
PhotoURL: u.User.PhotoURL,
Role: u.Role,
IsMe: u.UserID == userID,
})
}
c.JSON(http.StatusOK, response)
}
type UpdateUserRoleDTO struct {
NewRole string `json:"new_role" binding:"required"` // ADMIN, OPERATOR
}
func (h *SettingsHandler) UpdateUserRole(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
targetUserID, err := uuid.Parse(c.Param("userId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target user id"})
return
}
var req UpdateUserRoleDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
newRole := account.Role(req.NewRole)
if newRole != account.RoleAdmin && newRole != account.RoleOperator {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid role (allowed: ADMIN, OPERATOR)"})
return
}
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
return
}
myRole, _ := h.accountRepo.GetUserRole(userID, server.ID)
if myRole != account.RoleOwner {
c.JSON(http.StatusForbidden, gin.H{"error": "Only OWNER can change roles"})
return
}
if userID == targetUserID {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change own role"})
return
}
if err := h.accountRepo.SetUserRole(server.ID, targetUserID, newRole); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// --- УВЕДОМЛЕНИЕ О СМЕНЕ РОЛИ ---
if h.notifier != nil {
go func() {
users, err := h.accountRepo.GetServerUsers(server.ID)
if err == nil {
for _, u := range users {
if u.UserID == targetUserID {
h.notifier.SendRoleChangeNotification(u.User.TelegramID, server.Name, string(newRole))
break
}
}
}
}()
}
c.JSON(http.StatusOK, gin.H{"status": "updated"})
}
func (h *SettingsHandler) RemoveUser(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
targetUserID, err := uuid.Parse(c.Param("userId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target user id"})
return
}
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
return
}
myRole, _ := h.accountRepo.GetUserRole(userID, server.ID)
if myRole != account.RoleOwner && myRole != account.RoleAdmin {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
if userID == targetUserID {
c.JSON(http.StatusBadRequest, gin.H{"error": "Use 'leave' function instead"})
return
}
// Ищем цель в списке, чтобы проверить права и получить TelegramID для уведомления
users, _ := h.accountRepo.GetServerUsers(server.ID)
var targetTgID int64
var found bool
for _, u := range users {
if u.UserID == targetUserID {
found = true
targetTgID = u.User.TelegramID
if u.Role == account.RoleOwner {
c.JSON(http.StatusForbidden, gin.H{"error": "Cannot remove Owner"})
return
}
if myRole == account.RoleAdmin && u.Role == account.RoleAdmin {
c.JSON(http.StatusForbidden, gin.H{"error": "Admins cannot remove other Admins"})
return
}
break
}
}
if !found {
c.JSON(http.StatusNotFound, gin.H{"error": "Target user not found on server"})
return
}
if err := h.accountRepo.RemoveUserFromServer(server.ID, targetUserID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// --- УВЕДОМЛЕНИЕ ОБ УДАЛЕНИИ ---
if h.notifier != nil && targetTgID != 0 {
go h.notifier.SendRemovalNotification(targetTgID, server.Name)
}
c.JSON(http.StatusOK, gin.H{"status": "removed"})
}
// --- Server Management ---
// ServerShortDTO - краткая информация о сервере для списка
type ServerShortDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"` // OWNER, ADMIN, OPERATOR
IsActive bool `json:"is_active"`
}
// GetUserServers возвращает список всех серверов пользователя с ролями и флагом активности
func (h *SettingsHandler) GetUserServers(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
servers, err := h.accountRepo.GetAllAvailableServers(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
activeServer, err := h.accountRepo.GetActiveServer(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var activeServerID *uuid.UUID
if activeServer != nil {
activeServerID = &activeServer.ID
}
response := make([]ServerShortDTO, 0, len(servers))
for _, server := range servers {
role, err := h.accountRepo.GetUserRole(userID, server.ID)
if err != nil {
role = account.RoleOperator
}
response = append(response, ServerShortDTO{
ID: server.ID.String(),
Name: server.Name,
Role: string(role),
IsActive: activeServerID != nil && server.ID == *activeServerID,
})
}
c.JSON(http.StatusOK, response)
}
// SwitchActiveServerRequest - запрос на переключение активного сервера
type SwitchActiveServerRequest struct {
ServerID string `json:"server_id" binding:"required"`
}
// SwitchActiveServer переключает активный сервер пользователя
func (h *SettingsHandler) SwitchActiveServer(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
var req SwitchActiveServerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
serverID, err := uuid.Parse(req.ServerID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid server_id format"})
return
}
// Проверяем, что сервер доступен пользователю
servers, err := h.accountRepo.GetAllAvailableServers(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var serverExists bool
for _, s := range servers {
if s.ID == serverID {
serverExists = true
break
}
}
if !serverExists {
c.JSON(http.StatusNotFound, gin.H{"error": "server not found or not accessible"})
return
}
// Переключаем активный сервер
if err := h.accountRepo.SetActiveServer(userID, serverID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Сбрасываем кэш RMS клиента
if h.rmsFactory != nil {
h.rmsFactory.ClearCacheForUser(userID)
}
c.JSON(http.StatusOK, gin.H{"status": "active_server_changed"})
}